├── images ├── .gitkeep ├── adapter_pcb.jpg ├── wiring_screen.png ├── flipper_with_sensor.jpg ├── main_screen_explanation.jpg └── flipper_zero_uv_meter_outside.jpg ├── CHANGELOG.md ├── uv_meter.png ├── assets ├── Alert_9x8.png ├── Sun_15x16.png ├── Glasses_24x8.png ├── Wiring_128x64.png ├── ButtonCenter_7x7.png ├── Sunglasses_24x8.png ├── Unit_W_m2_22x11.png ├── Unit_mW_m2_28x11.png ├── ButtonLeftSmall_3x5.png ├── Unit_uW_cm2_34x11.png └── ButtonRightSmall_3x5.png ├── .gitignore ├── screenshots ├── data_1.png ├── data_2.png ├── wiring.png └── settings.png ├── adapter_pcb ├── gerber_v1.0.zip └── flipper_as7331_adapter │ ├── .gitignore │ ├── flipper_as7331_adapter.kicad_pro │ └── flipper_as7331_adapter.kicad_sch ├── magic_numbers ├── visualize_data.jpeg ├── weighted_spectral_effectiveness_eyes_protected.jpeg ├── weighted_spectral_effectiveness_eyes_not_protected.jpeg ├── Pipfile ├── requirements.txt ├── uv_responsivity_and_spectral_effectiveness_data.csv ├── visualize_data.py ├── README.md └── weighted_spectral_effectiveness.py ├── uv_meter_event.hpp ├── scenes ├── uv_meter_scene_config.hpp ├── uv_meter_scene.hpp ├── uv_meter_scene.cpp ├── uv_meter_scene_help.cpp ├── uv_meter_scene_about.cpp ├── uv_meter_scene_wiring.cpp ├── uv_meter_scene_settings.cpp └── uv_meter_scene_data.cpp ├── views ├── uv_meter_wiring.hpp ├── uv_meter_data.hpp ├── uv_meter_wiring.cpp └── uv_meter_data.cpp ├── application.fam ├── uv_meter_app.hpp ├── uv_meter_app_i.hpp ├── .github └── workflows │ └── build.yml ├── DESCRIPTION.md ├── uv_meter_app.cpp ├── README.md ├── AS7331.hpp └── LICENSE /images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v1.0: 2 | Initial Release -------------------------------------------------------------------------------- /uv_meter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/uv_meter.png -------------------------------------------------------------------------------- /assets/Alert_9x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/assets/Alert_9x8.png -------------------------------------------------------------------------------- /assets/Sun_15x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/assets/Sun_15x16.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | .vscode 3 | .clang-format 4 | .clangd 5 | .editorconfig 6 | .env 7 | .ufbt 8 | -------------------------------------------------------------------------------- /images/adapter_pcb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/images/adapter_pcb.jpg -------------------------------------------------------------------------------- /screenshots/data_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/screenshots/data_1.png -------------------------------------------------------------------------------- /screenshots/data_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/screenshots/data_2.png -------------------------------------------------------------------------------- /screenshots/wiring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/screenshots/wiring.png -------------------------------------------------------------------------------- /assets/Glasses_24x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/assets/Glasses_24x8.png -------------------------------------------------------------------------------- /assets/Wiring_128x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/assets/Wiring_128x64.png -------------------------------------------------------------------------------- /images/wiring_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/images/wiring_screen.png -------------------------------------------------------------------------------- /screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/screenshots/settings.png -------------------------------------------------------------------------------- /adapter_pcb/gerber_v1.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/adapter_pcb/gerber_v1.0.zip -------------------------------------------------------------------------------- /assets/ButtonCenter_7x7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/assets/ButtonCenter_7x7.png -------------------------------------------------------------------------------- /assets/Sunglasses_24x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/assets/Sunglasses_24x8.png -------------------------------------------------------------------------------- /assets/Unit_W_m2_22x11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/assets/Unit_W_m2_22x11.png -------------------------------------------------------------------------------- /assets/Unit_mW_m2_28x11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/assets/Unit_mW_m2_28x11.png -------------------------------------------------------------------------------- /assets/ButtonLeftSmall_3x5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/assets/ButtonLeftSmall_3x5.png -------------------------------------------------------------------------------- /assets/Unit_uW_cm2_34x11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/assets/Unit_uW_cm2_34x11.png -------------------------------------------------------------------------------- /images/flipper_with_sensor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/images/flipper_with_sensor.jpg -------------------------------------------------------------------------------- /assets/ButtonRightSmall_3x5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/assets/ButtonRightSmall_3x5.png -------------------------------------------------------------------------------- /images/main_screen_explanation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/images/main_screen_explanation.jpg -------------------------------------------------------------------------------- /magic_numbers/visualize_data.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/magic_numbers/visualize_data.jpeg -------------------------------------------------------------------------------- /images/flipper_zero_uv_meter_outside.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/images/flipper_zero_uv_meter_outside.jpg -------------------------------------------------------------------------------- /magic_numbers/weighted_spectral_effectiveness_eyes_protected.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/magic_numbers/weighted_spectral_effectiveness_eyes_protected.jpeg -------------------------------------------------------------------------------- /magic_numbers/weighted_spectral_effectiveness_eyes_not_protected.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbaisch/uv_meter/HEAD/magic_numbers/weighted_spectral_effectiveness_eyes_not_protected.jpeg -------------------------------------------------------------------------------- /uv_meter_event.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef enum { 4 | UVMeterCustomEventSceneEnterHelp, 5 | UVMeterCustomEventSceneEnterAbout, 6 | UVMeterCustomEventSceneEnterSettings, 7 | } UVMeterCustomEvent; 8 | -------------------------------------------------------------------------------- /scenes/uv_meter_scene_config.hpp: -------------------------------------------------------------------------------- 1 | ADD_SCENE(uv_meter, wiring, Wiring) 2 | ADD_SCENE(uv_meter, data, Data) 3 | ADD_SCENE(uv_meter, settings, Settings) 4 | ADD_SCENE(uv_meter, about, About) 5 | ADD_SCENE(uv_meter, help, Help) 6 | -------------------------------------------------------------------------------- /magic_numbers/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | pandas = "*" 8 | matplotlib = "*" 9 | 10 | [dev-packages] 11 | 12 | [requires] 13 | python_version = "3.13" 14 | -------------------------------------------------------------------------------- /views/uv_meter_wiring.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct UVMeterWiring UVMeterWiring; 6 | typedef void (*UVMeterWiringEnterSettingsCallback)(void* context); 7 | 8 | UVMeterWiring* uv_meter_wiring_alloc(void); 9 | void uv_meter_wiring_free(UVMeterWiring* instance); 10 | View* uv_meter_wiring_get_view(UVMeterWiring* instance); 11 | 12 | void uv_meter_wiring_set_enter_settings_callback( 13 | UVMeterWiring* instance, 14 | UVMeterWiringEnterSettingsCallback callback, 15 | void* context); 16 | -------------------------------------------------------------------------------- /application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="uv_meter_as7331", 3 | name="UV Meter", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="uv_meter_app", 6 | requires=["gui"], 7 | stack_size=2 * 1024, 8 | fap_category="GPIO", 9 | sources=["*.c*", "!magic_numbers"], 10 | fap_version="1.0", 11 | fap_icon="uv_meter.png", 12 | fap_description="Measure UV radiation using the AS7331 sensor", 13 | fap_author="Michael Baisch", 14 | fap_weburl="https://github.com/michaelbaisch/uv_meter", 15 | fap_icon_assets="assets", 16 | ) 17 | -------------------------------------------------------------------------------- /adapter_pcb/flipper_as7331_adapter/.gitignore: -------------------------------------------------------------------------------- 1 | # For PCBs designed using KiCad: https://www.kicad.org/ 2 | # Format documentation: https://kicad.org/help/file-formats/ 3 | 4 | # Temporary files 5 | *.000 6 | *.bak 7 | *.bck 8 | *.kicad_pcb-bak 9 | *.kicad_sch-bak 10 | *-backups 11 | *.kicad_prl 12 | *.sch-bak 13 | *~ 14 | _autosave-* 15 | *.tmp 16 | *-save.pro 17 | *-save.kicad_pcb 18 | fp-info-cache 19 | ~*.lck 20 | \#auto_saved_files# 21 | 22 | # Netlist files (exported from Eeschema) 23 | *.net 24 | 25 | # Autorouter files (exported from Pcbnew) 26 | *.dsn 27 | *.ses 28 | 29 | # Exported BOM files 30 | *.xml 31 | *.csv 32 | -------------------------------------------------------------------------------- /magic_numbers/requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | contourpy==1.3.1; python_version >= '3.10' 3 | cycler==0.12.1; python_version >= '3.8' 4 | fonttools==4.56.0; python_version >= '3.8' 5 | kiwisolver==1.4.8; python_version >= '3.10' 6 | matplotlib==3.10.1; python_version >= '3.10' 7 | numpy==2.2.3; python_version >= '3.10' 8 | packaging==24.2; python_version >= '3.8' 9 | pandas==2.2.3; python_version >= '3.9' 10 | pillow==11.1.0; python_version >= '3.9' 11 | pyparsing==3.2.1; python_version >= '3.9' 12 | python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' 13 | pytz==2025.1 14 | six==1.17.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' 15 | tzdata==2025.1; python_version >= '2' 16 | -------------------------------------------------------------------------------- /uv_meter_app.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file uv_meter_app.hpp 3 | * @brief UV Meter Application for AS7331 UV Spectral Sensor 4 | * 5 | * This application interfaces with the AS7331 UV spectral sensor using I2C communication 6 | * to measure UV-A, UV-B, and UV-C irradiance. The measurements are displayed on the Flipper Zero's screen. 7 | * 8 | * Hardware Connections: 9 | * - SCL: C0 [pin 16] 10 | * - SDA: C1 [pin 15] 11 | * - 3V3: 3V3 [pin 9] 12 | * - GND: GND [pin 11 or 18] 13 | */ 14 | #pragma once 15 | 16 | typedef struct UVMeterApp UVMeterApp; 17 | 18 | typedef enum { 19 | UVMeterI2CAddressAuto, 20 | UVMeterI2CAddress74, 21 | UVMeterI2CAddress75, 22 | UVMeterI2CAddress76, 23 | UVMeterI2CAddress77, 24 | } UVMeterI2CAddress; 25 | 26 | typedef enum { 27 | UVMeterUnituW_cm_2, 28 | UVMeterUnitW_m_2, 29 | UVMeterUnitmW_m_2, 30 | } UVMeterUnit; 31 | -------------------------------------------------------------------------------- /scenes/uv_meter_scene.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // Generate scene id and total number 6 | #define ADD_SCENE(prefix, name, id) UVMeterScene##id, 7 | typedef enum { 8 | #include "uv_meter_scene_config.hpp" 9 | UVMeterSceneNum, 10 | } UVMeterScene; 11 | #undef ADD_SCENE 12 | 13 | extern const SceneManagerHandlers uv_meter_scene_handlers; 14 | 15 | // Generate scene on_enter handlers declaration 16 | #define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); 17 | #include "uv_meter_scene_config.hpp" 18 | #undef ADD_SCENE 19 | 20 | // Generate scene on_event handlers declaration 21 | #define ADD_SCENE(prefix, name, id) \ 22 | bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); 23 | #include "uv_meter_scene_config.hpp" 24 | #undef ADD_SCENE 25 | 26 | // Generate scene on_exit handlers declaration 27 | #define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context); 28 | #include "uv_meter_scene_config.hpp" 29 | #undef ADD_SCENE -------------------------------------------------------------------------------- /scenes/uv_meter_scene.cpp: -------------------------------------------------------------------------------- 1 | #include "uv_meter_scene.hpp" 2 | 3 | // Generate scene on_enter handlers array 4 | #define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, 5 | void (*const uv_meter_scene_on_enter_handlers[])(void*) = { 6 | #include "uv_meter_scene_config.hpp" 7 | }; 8 | #undef ADD_SCENE 9 | 10 | // Generate scene on_event handlers array 11 | #define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, 12 | bool (*const uv_meter_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = { 13 | #include "uv_meter_scene_config.hpp" 14 | }; 15 | #undef ADD_SCENE 16 | 17 | // Generate scene on_exit handlers array 18 | #define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, 19 | void (*const uv_meter_scene_on_exit_handlers[])(void* context) = { 20 | #include "uv_meter_scene_config.hpp" 21 | }; 22 | #undef ADD_SCENE 23 | 24 | // Initialize scene handlers configuration structure 25 | const SceneManagerHandlers uv_meter_scene_handlers = { 26 | .on_enter_handlers = uv_meter_scene_on_enter_handlers, 27 | .on_event_handlers = uv_meter_scene_on_event_handlers, 28 | .on_exit_handlers = uv_meter_scene_on_exit_handlers, 29 | .scene_num = UVMeterSceneNum, 30 | }; -------------------------------------------------------------------------------- /magic_numbers/uv_responsivity_and_spectral_effectiveness_data.csv: -------------------------------------------------------------------------------- 1 | Wavelength (nm),UVC,UVB,UVA,Relative Spectral Effectiveness,Relative Spectral Effectiveness (eyes protected) 2 | 230,0.0,0.0,0.03,0.19,0.0189 3 | 235,0.0,0.0,0.03,0.24,0.038 4 | 240,0.25,0.0,0.02,0.3,0.075 5 | 245,0.88,0.0,0.02,0.36,0.15 6 | 250,1.0,0.0,0.02,0.43,0.3 7 | 255,0.92,0.0,0.0,0.52,0.3 8 | 260,0.89,0.0,0.0,0.65,0.3 9 | 265,0.86,0.03,0.0,0.81,0.3 10 | 270,0.8,0.04,0.0,1.0,0.3 11 | 275,0.67,0.08,0.0,0.96,0.3 12 | 280,0.48,0.37,0.0,0.88,0.3 13 | 285,0.15,0.83,0.0,0.77,0.3 14 | 290,0.0,0.9,0.0,0.64,0.3 15 | 295,0.0,1.0,0.0,0.54,0.3 16 | 300,0.0,0.95,0.02,0.3,0.3 17 | 305,0.0,0.89,0.03,0.06,0.06 18 | 310,0.0,0.8,0.06,0.015,0.015 19 | 315,0.0,0.4,0.31,0.003,0.003 20 | 320,0.0,0.1,0.82,0.001,0.001 21 | 325,0.0,0.0,0.89,0.0005,0.0005 22 | 330,0.0,0.0,0.9,0.00041,0.00041 23 | 335,0.0,0.0,1.0,0.00034,0.00034 24 | 340,0.0,0.0,0.9,0.00028,0.00028 25 | 345,0.0,0.0,1.0,0.00024,0.00024 26 | 350,0.0,0.0,0.86,0.0002,0.0002 27 | 355,0.0,0.0,0.75,0.00016,0.00016 28 | 360,0.0,0.0,0.75,0.00013,0.00013 29 | 365,0.0,0.0,0.62,0.00011,0.00011 30 | 370,0.0,0.0,0.75,9.3e-05,9.3e-05 31 | 375,0.0,0.0,0.61,7.7e-05,7.7e-05 32 | 380,0.0,0.0,0.75,6.4e-05,6.4e-05 33 | 385,0.0,0.0,0.6,5.3e-05,5.3e-05 34 | 390,0.0,0.0,0.67,4.4e-05,4.4e-05 35 | 395,0.0,0.0,0.52,3.6e-05,3.6e-05 36 | 400,0.0,0.0,0.55,3e-05,3e-05 37 | 405,0.0,0.0,0.43,3e-05,3e-05 38 | 410,0.0,0.0,0.4,3e-05,3e-05 39 | 415,0.0,0.0,0.2,3e-05,3e-05 40 | 420,0.0,0.0,0.03,3e-05,3e-05 41 | 425,0.0,0.0,0.0,3e-05,3e-05 42 | 430,0.0,0.0,0.0,3e-05,3e-05 43 | -------------------------------------------------------------------------------- /scenes/uv_meter_scene_help.cpp: -------------------------------------------------------------------------------- 1 | #include "uv_meter_app_i.hpp" 2 | 3 | void uv_meter_scene_help_on_enter(void* context) { 4 | furi_assert(context); 5 | auto* app = static_cast(context); 6 | widget_reset(app->widget); 7 | FuriString* tmp_string = furi_string_alloc(); 8 | 9 | furi_string_cat_str(tmp_string, "\e#Wiring:\n"); 10 | furi_string_cat_str( 11 | tmp_string, 12 | " SCL: 16 [C0]\n" 13 | " SDA: 15 [C1]\n" 14 | " 3V3: 9 [3V3]\n" 15 | " GND: 11 or 18 [GND]\n"); 16 | furi_string_cat_str(tmp_string, "\e#Usage:\n"); 17 | furi_string_cat_str( 18 | tmp_string, 19 | "Main UV values shown with\n" 20 | "raw value meters beside.\n" 21 | "Avoid low/high warnings by\n" 22 | "adjusting Gain/Exposure.\n" 23 | "Right side: Max daily safe\n" 24 | "UV exposure (minutes per\n" 25 | "8h day).\n" 26 | "Percentages indicates each\n" 27 | "UV type's contribution.\n"); 28 | furi_string_cat_str( 29 | tmp_string, 30 | "\e#Disclaimer\n" 31 | "Info provided as-is;\n" 32 | "not liable for usage.\n"); 33 | 34 | widget_add_text_scroll_element(app->widget, 0, 0, 128, 64, furi_string_get_cstr(tmp_string)); 35 | 36 | furi_string_free(tmp_string); 37 | view_dispatcher_switch_to_view(app->view_dispatcher, UVMeterViewWidget); 38 | } 39 | 40 | bool uv_meter_scene_help_on_event(void* context, SceneManagerEvent event) { 41 | UNUSED(context); 42 | UNUSED(event); 43 | return false; 44 | } 45 | void uv_meter_scene_help_on_exit(void* context) { 46 | furi_assert(context); 47 | auto* app = static_cast(context); 48 | widget_reset(app->widget); 49 | } -------------------------------------------------------------------------------- /uv_meter_app_i.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "uv_meter_app.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | #include "uv_meter_as7331_icons.h" 18 | #include "scenes/uv_meter_scene.hpp" 19 | #include "views/uv_meter_wiring.hpp" 20 | #include "views/uv_meter_data.hpp" 21 | #include "AS7331.hpp" 22 | 23 | // Prevent compiler optimization for debugging (remove in production). 24 | // #pragma GCC optimize("O0") 25 | 26 | typedef struct { 27 | AS7331::Results results; /**< Processed measurement results */ 28 | AS7331::RawResults raw_results; /**< Raw measurement results */ 29 | AS7331 as7331; /**< AS7331 sensor object */ 30 | bool as7331_initialized; /**< Flag indicating if sensor is initialized */ 31 | FuriMutex* as7331_mutex; /**< Mutex for thread-safe data access */ 32 | uint32_t last_sensor_check_timestamp; /**< Last time sensor availability was checked */ 33 | // Settings 34 | UVMeterI2CAddress i2c_address; 35 | UVMeterUnit unit; 36 | } UVMeterAppState; 37 | 38 | struct UVMeterApp { 39 | Gui* gui; 40 | SceneManager* scene_manager; 41 | ViewDispatcher* view_dispatcher; 42 | // Views 43 | UVMeterWiring* uv_meter_wiring_view; 44 | VariableItemList* variable_item_list; 45 | UVMeterData* uv_meter_data_view; 46 | Widget* widget; 47 | 48 | UVMeterAppState* app_state; /**< Shared app_state */ 49 | }; 50 | 51 | typedef enum { 52 | UVMeterViewWiring, 53 | UVMeterViewVariableItemList, 54 | UVMeterViewData, 55 | UVMeterViewWidget, 56 | } UVMeterView; 57 | -------------------------------------------------------------------------------- /scenes/uv_meter_scene_about.cpp: -------------------------------------------------------------------------------- 1 | #include "uv_meter_app_i.hpp" 2 | 3 | #define UV_METER_VERSION_APP FAP_VERSION 4 | #define UV_METER_DEVELOPER "Michael Baisch" 5 | #define UV_METER_GITHUB "https://github.com/michaelbaisch/uv_meter" 6 | #define UV_METER_NAME "\e#\e! UV Meter \e!\n" 7 | #define UV_METER_BLANK_INV "\e#\e! \e!\n" 8 | 9 | void uv_meter_scene_about_on_enter(void* context) { 10 | furi_assert(context); 11 | auto* app = static_cast(context); 12 | widget_reset(app->widget); 13 | FuriString* tmp_string = furi_string_alloc(); 14 | 15 | widget_add_text_box_element( 16 | app->widget, 0, 0, 128, 14, AlignCenter, AlignBottom, UV_METER_BLANK_INV, false); 17 | widget_add_text_box_element( 18 | app->widget, 0, 2, 128, 14, AlignCenter, AlignBottom, UV_METER_NAME, false); 19 | furi_string_printf(tmp_string, "\e#Information\n"); 20 | furi_string_cat_printf(tmp_string, "Version: %s\n", UV_METER_VERSION_APP); 21 | furi_string_cat_printf(tmp_string, "Developed by: %s\n", UV_METER_DEVELOPER); 22 | furi_string_cat_printf(tmp_string, "Github: %s\n\n", UV_METER_GITHUB); 23 | furi_string_cat_str(tmp_string, "\e#Description\n"); 24 | furi_string_cat_str(tmp_string, "Measure UV radiation using\n"); 25 | furi_string_cat_str(tmp_string, "the AS7331 sensor.\n"); 26 | widget_add_text_scroll_element(app->widget, 0, 16, 128, 50, furi_string_get_cstr(tmp_string)); 27 | 28 | furi_string_free(tmp_string); 29 | view_dispatcher_switch_to_view(app->view_dispatcher, UVMeterViewWidget); 30 | } 31 | 32 | bool uv_meter_scene_about_on_event(void* context, SceneManagerEvent event) { 33 | UNUSED(context); 34 | UNUSED(event); 35 | return false; 36 | } 37 | void uv_meter_scene_about_on_exit(void* context) { 38 | furi_assert(context); 39 | auto* app = static_cast(context); 40 | widget_reset(app->widget); 41 | } -------------------------------------------------------------------------------- /views/uv_meter_data.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "AS7331.hpp" 5 | #include "uv_meter_app.hpp" 6 | 7 | typedef struct UVMeterData UVMeterData; 8 | typedef void (*UVMeterDataEnterSettingsCallback)(void* context); 9 | 10 | /** @brief Struct to hold effective irradiance and daily exposure time */ 11 | typedef struct { 12 | double uv_a_eff; /**< Effective UV-A Irradiance in µW/cm² */ 13 | double uv_b_eff; /**< Effective UV-B Irradiance in µW/cm² */ 14 | double uv_c_eff; /**< Effective UV-C Irradiance in µW/cm² */ 15 | double uv_total_eff; /**< Effective total UV Irradiance in µW/cm² */ 16 | double t_max; /**< Maximum Daily Exposure Time in seconds*/ 17 | } UVMeterEffectiveResults; 18 | 19 | UVMeterData* uv_meter_data_alloc(void); 20 | void uv_meter_data_free(UVMeterData* instance); 21 | void uv_meter_data_reset(UVMeterData* instance, bool update = false); 22 | 23 | View* uv_meter_data_get_view(UVMeterData* instance); 24 | 25 | void uv_meter_data_set_enter_settings_callback( 26 | UVMeterData* instance, 27 | UVMeterDataEnterSettingsCallback callback, 28 | void* context); 29 | 30 | // AS7331 Sensor 31 | void uv_meter_data_set_sensor(UVMeterData* instance, AS7331* sensor, FuriMutex* sensor_mutex); 32 | void uv_meter_update_from_sensor(UVMeterData* instance); 33 | 34 | // General getter and setter 35 | void uv_meter_data_set_results( 36 | UVMeterData* instance, 37 | const AS7331::Results* results, 38 | const AS7331::RawResults* raw_results); 39 | UVMeterEffectiveResults uv_meter_data_get_effective_results(UVMeterData* instance); 40 | void uv_meter_data_set_eyes_protected(UVMeterData* instance, bool eyes_protected); 41 | bool uv_meter_data_get_eyes_protected(UVMeterData* instance); 42 | void uv_meter_data_set_unit(UVMeterData* instance, UVMeterUnit unit); 43 | 44 | // Helper 45 | UVMeterEffectiveResults 46 | uv_meter_data_calculate_effective_results(const AS7331::Results* results, bool eyes_protected); 47 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "UV Meter: Build for multiple SDK sources" 2 | # This will build your app for dev and release channels on GitHub. 3 | # It will also build your app every day to make sure it's up to date with the latest SDK changes. 4 | # See https://github.com/marketplace/actions/build-flipper-application-package-fap for more information 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - '**' 13 | schedule: 14 | # do a build every day 15 | - cron: "1 1 * * *" 16 | 17 | jobs: 18 | ufbt-build: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | include: 23 | - name: Official Dev channel 24 | sdk-channel: dev 25 | - name: Official Release channel 26 | sdk-channel: release 27 | - name: Unleashed Dev 28 | sdk-index-url: https://up.unleashedflip.com/directory.json 29 | sdk-channel: dev 30 | - name: Unleashed Release 31 | sdk-index-url: https://up.unleashedflip.com/directory.json 32 | sdk-channel: release 33 | - name: Momentum Dev 34 | sdk-index-url: https://up.momentum-fw.dev/firmware/directory.json 35 | sdk-channel: dev 36 | - name: Momentum Release 37 | sdk-index-url: https://up.momentum-fw.dev/firmware/directory.json 38 | sdk-channel: release 39 | name: 'ufbt: Build for ${{ matrix.name }}' 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | - name: Build with ufbt 44 | uses: flipperdevices/flipperzero-ufbt-action@v0.1.3 45 | id: build-app 46 | with: 47 | sdk-channel: ${{ matrix.sdk-channel }} 48 | sdk-index-url: ${{ matrix.sdk-index-url }} 49 | - name: Upload app artifacts 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }} 53 | path: ${{ steps.build-app.outputs.fap-artifacts }} 54 | -------------------------------------------------------------------------------- /views/uv_meter_wiring.cpp: -------------------------------------------------------------------------------- 1 | #include "uv_meter_wiring.hpp" 2 | 3 | #include 4 | #include "uv_meter_as7331_icons.h" 5 | 6 | #include 7 | #include 8 | 9 | #define UV_METER_MAX_RAW_VALUE 65535.0 10 | 11 | struct UVMeterWiring { 12 | View* view; 13 | UVMeterWiringEnterSettingsCallback callback; 14 | void* context; 15 | //IconAnimation* icon; 16 | }; 17 | 18 | typedef struct { 19 | IconAnimation* icon; 20 | } UVMeterWiringModel; 21 | 22 | static void uv_meter_wiring_draw_callback(Canvas* canvas, void* model) { 23 | UNUSED(model); 24 | //auto* m = static_cast(model); 25 | FURI_LOG_D("UV_Meter Wiring", "Redrawing"); 26 | 27 | canvas_draw_icon(canvas, 0, 0, &I_Wiring_128x64); 28 | } 29 | 30 | static bool uv_meter_wiring_input_callback(InputEvent* event, void* context) { 31 | auto* instance = static_cast(context); 32 | bool consumed = false; 33 | 34 | if(event->key == InputKeyOk && event->type == InputTypeShort && instance->callback) { 35 | instance->callback(instance->context); 36 | consumed = true; 37 | } 38 | return consumed; 39 | } 40 | 41 | UVMeterWiring* uv_meter_wiring_alloc(void) { 42 | UVMeterWiring* instance = new UVMeterWiring(); 43 | instance->view = view_alloc(); 44 | view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(UVMeterWiringModel)); 45 | view_set_draw_callback(instance->view, uv_meter_wiring_draw_callback); 46 | view_set_input_callback(instance->view, uv_meter_wiring_input_callback); 47 | view_set_context(instance->view, instance); 48 | return instance; 49 | } 50 | 51 | void uv_meter_wiring_free(UVMeterWiring* instance) { 52 | furi_assert(instance); 53 | view_free(instance->view); 54 | delete instance; 55 | } 56 | 57 | View* uv_meter_wiring_get_view(UVMeterWiring* instance) { 58 | furi_assert(instance); 59 | return instance->view; 60 | } 61 | 62 | void uv_meter_wiring_set_enter_settings_callback( 63 | UVMeterWiring* instance, 64 | UVMeterWiringEnterSettingsCallback callback, 65 | void* context) { 66 | furi_assert(instance); 67 | furi_assert(callback); 68 | with_view_model_cpp( 69 | instance->view, 70 | UVMeterWiringModel*, 71 | model, 72 | { 73 | UNUSED(model); 74 | instance->callback = callback; 75 | instance->context = context; 76 | }, 77 | false); 78 | } 79 | -------------------------------------------------------------------------------- /DESCRIPTION.md: -------------------------------------------------------------------------------- 1 | # UV Meter 2 | 3 | Application designed to measure ultraviolet (UV) radiation levels using the [AS7331](https://ams-osram.com/products/sensor-solutions/ambient-light-color-spectral-proximity-sensors/ams-as7331-spectral-uv-sensor) sensor. It supports individual measurements for UV-A, UV-B, and UV-C wavelengths. The easiest way to hook everything up is to use a breakout board, such as the one from [SparkFun](https://www.sparkfun.com/sparkfun-spectral-uv-sensor-as7331-qwiic.html). 4 | 5 | 6 | ## Wiring 7 | 8 | Connect the AS7331 sensor to your Flipper Zero via I²C: 9 | 10 | | Sensor Pin | Flipper Zero Pin | 11 | |------------|--------------------| 12 | | **SCL** | C0 \[pin 16\] | 13 | | **SDA** | C1 \[pin 15\] | 14 | | **3V3** | 3V3 \[pin 9\] | 15 | | **GND** | GND \[pin 11 or 18\] | 16 | 17 | By default, the application scans all possible I²C addresses for the sensor. However, you can manually set a specific address in the settings menu, accessible by pressing the **Enter** button. 18 | 19 | 20 | ## Usage 21 | 22 | Once connected, the application automatically displays real-time UV measurements. The main screen shows individual UV-A, UV-B, and UV-C readings. Beneath the numbers you see the currently used unit (µW/cm², W/m² or mW/m²). 23 | 24 | Next to each UV reading is a small meter indicating the raw sensor value. If this value is too high or too low, a warning symbol will appear, signaling potential sensor overexposure or underexposure. This condition will probably result in unreliable measurements. Adjusting the **Gain** and **Exposure Time** settings (similar to camera ISO and shutter speed) at the bottom of the screen can help correct this. 25 | 26 | In addition to direct sensor measurements, the application provides an interpretation of this irradiance data on human safety, displayed on the right side of the screen. The time (in minutes) indicates the recommended maximum daily exposure duration during an 8-hour work shift, based on the 2024 Threshold Limit Values (TLVs) and Biological Exposure Indices (BEIs) published by the [American Conference of Governmental Industrial Hygienists (ACGIH)](https://en.wikipedia.org/wiki/American_Conference_of_Governmental_Industrial_Hygienists). 27 | 28 | Since UV radiation poses greater risks to the eyes, the app includes a setting to specify whether your eyes are protected. This adjusts the maximum daily exposure duration according to ACGIH recommendations. 29 | 30 | The displayed percentages indicate how much each UV type (UV-A, UV-B, UV-C) contributes to the maximum daily exposure. This helps illustrate that higher sensor readings don't necessarily mean a greater health risk; for example, even though UV-A sensor values are typically higher, they usually contribute less to the recommended maximum exposure limit compared to UV-B or UV-C. 31 | 32 | When following the maximum daily exposure duration, the TLV/BEI guidelines ensure: “\[…\] nearly all healthy workers may be repeatedly exposed without acute adverse health effects such as erythema and photokeratitis.” 33 | 34 | 35 | ## Disclaimer 36 | 37 | This application is provided for informational purposes only and should not be used as a sole basis for safety-critical decisions. Always follow official guidelines, regulations, and professional advice regarding UV exposure. The developer assumes no responsibility for any damages, injuries, or consequences arising from decisions or actions based on the information provided by this application. 38 | -------------------------------------------------------------------------------- /scenes/uv_meter_scene_wiring.cpp: -------------------------------------------------------------------------------- 1 | #include "uv_meter_app_i.hpp" 2 | #include "uv_meter_event.hpp" 3 | 4 | static void uv_meter_scene_wiring_enter_settings_callback(void* context) { 5 | furi_assert(context); 6 | auto* app = static_cast(context); 7 | view_dispatcher_send_custom_event(app->view_dispatcher, UVMeterCustomEventSceneEnterSettings); 8 | } 9 | 10 | void uv_meter_scene_wiring_on_enter(void* context) { 11 | furi_assert(context); 12 | auto* app = static_cast(context); 13 | 14 | uv_meter_wiring_set_enter_settings_callback( 15 | app->uv_meter_wiring_view, uv_meter_scene_wiring_enter_settings_callback, app); 16 | 17 | view_dispatcher_switch_to_view(app->view_dispatcher, UVMeterViewWiring); 18 | } 19 | 20 | bool uv_meter_scene_wiring_on_event(void* context, SceneManagerEvent event) { 21 | furi_assert(context); 22 | auto* app = static_cast(context); 23 | bool consumed = false; 24 | 25 | if(event.type == SceneManagerEventTypeCustom) { 26 | if(event.event == UVMeterCustomEventSceneEnterSettings) { 27 | scene_manager_next_scene(app->scene_manager, UVMeterSceneSettings); 28 | consumed = true; 29 | } 30 | } else if(event.type == SceneManagerEventTypeTick) { 31 | uint32_t current_time = furi_get_tick(); 32 | 33 | // Check once per second if AS7331 sensor is available 34 | if(current_time - app->app_state->last_sensor_check_timestamp >= furi_ms_to_ticks(1000)) { 35 | furi_mutex_acquire(app->app_state->as7331_mutex, FuriWaitForever); 36 | 37 | if(!app->app_state->as7331_initialized) { 38 | // Initialize the sensor, also wake it up if in power down mode 39 | uint8_t i2c_address = 0x0; 40 | switch(app->app_state->i2c_address) { 41 | case UVMeterI2CAddressAuto: 42 | break; 43 | case UVMeterI2CAddress74: 44 | i2c_address = DefaultI2CAddr; 45 | break; 46 | case UVMeterI2CAddress75: 47 | i2c_address = SecondaryI2CAddr; 48 | break; 49 | case UVMeterI2CAddress76: 50 | i2c_address = TertiaryI2CAddr; 51 | break; 52 | case UVMeterI2CAddress77: 53 | i2c_address = QuaternaryI2CAddr; 54 | break; 55 | } 56 | app->app_state->as7331_initialized = app->app_state->as7331.init(i2c_address); 57 | } 58 | if(!app->app_state->as7331_initialized || !app->app_state->as7331.deviceReady()) { 59 | app->app_state->as7331_initialized = false; 60 | } else { 61 | // Set default Gain and Integration Time 62 | app->app_state->as7331.setGain(GAIN_8); 63 | app->app_state->as7331.setIntegrationTime(TIME_128MS); 64 | } 65 | furi_mutex_release(app->app_state->as7331_mutex); 66 | 67 | if(app->app_state->as7331_initialized) { 68 | scene_manager_next_scene(app->scene_manager, UVMeterSceneData); 69 | consumed = true; 70 | } 71 | 72 | app->app_state->last_sensor_check_timestamp = current_time; 73 | } 74 | 75 | } else if(event.type == SceneManagerEventTypeBack) { 76 | // Always quit app in wiring scene 77 | scene_manager_stop(app->scene_manager); 78 | view_dispatcher_stop(app->view_dispatcher); 79 | consumed = true; 80 | } 81 | 82 | return consumed; 83 | } 84 | 85 | void uv_meter_scene_wiring_on_exit(void* context) { 86 | UNUSED(context); 87 | } 88 | -------------------------------------------------------------------------------- /magic_numbers/visualize_data.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | # ----------------------------------------------------- 6 | # 1) Read and prepare data 7 | # ----------------------------------------------------- 8 | file_path = "./uv_responsivity_and_spectral_effectiveness_data.csv" 9 | df = pd.read_csv(file_path) 10 | 11 | w = df["Wavelength (nm)"].values 12 | uvc = df["UVC"].values 13 | uvb = df["UVB"].values 14 | uva = df["UVA"].values 15 | eff_std = df["Relative Spectral Effectiveness"].values 16 | eff_prot = df["Relative Spectral Effectiveness (eyes protected)"].values 17 | 18 | # ----------------------------------------------------- 19 | # 2) Peak finding (with UVA “double-peak” averaging) 20 | # ----------------------------------------------------- 21 | peak_uvc_idx = np.nanargmax(uvc) 22 | peak_uvb_idx = np.nanargmax(uvb) 23 | 24 | uva_ones = w[uva == 1.0] 25 | if len(uva_ones) >= 2: 26 | avg_uva_wl = (uva_ones[0] + uva_ones[1]) / 2 27 | peak_uva_idx = np.abs(w - avg_uva_wl).argmin() 28 | else: 29 | peak_uva_idx = np.nanargmax(uva) 30 | 31 | peak_uvc_wl = w[peak_uvc_idx] 32 | peak_uvb_wl = w[peak_uvb_idx] 33 | peak_uva_wl = w[peak_uva_idx] 34 | 35 | peak_uvc_eff_std = eff_std[peak_uvc_idx] 36 | peak_uvc_eff_prot = eff_prot[peak_uvc_idx] 37 | peak_uvb_eff_std = eff_std[peak_uvb_idx] 38 | peak_uvb_eff_prot = eff_prot[peak_uvb_idx] 39 | peak_uva_eff_std = eff_std[peak_uva_idx] 40 | peak_uva_eff_prot = eff_prot[peak_uva_idx] 41 | 42 | # ----------------------------------------------------- 43 | # 3) Plotting 44 | # ----------------------------------------------------- 45 | colors = { 46 | "UVC": "#4d4d4d", # darker grey 47 | "UVB": "#8a2be2", # violet/purple 48 | "UVA": "#0b66f2", # pleasing blue 49 | } 50 | 51 | fig, ax1 = plt.subplots(figsize=(10, 6)) 52 | 53 | # 3.1) Sensor responsivity curves 54 | ax1.plot(w, uvc, color=colors["UVC"], linewidth=2, label="UVC Responsivity") 55 | ax1.plot(w, uvb, color=colors["UVB"], linewidth=2, label="UVB Responsivity") 56 | ax1.plot(w, uva, color=colors["UVA"], linewidth=2, label="UVA Responsivity") 57 | 58 | # 3.2) Peaks: vertical lines + marker + annotate SpecEff std/prot 59 | y_annot = 1.05 60 | for wl, color, eff_s, eff_p, lbl in [ 61 | (peak_uvc_wl, colors["UVC"], peak_uvc_eff_std, peak_uvc_eff_prot, "UVC Peak"), 62 | (peak_uvb_wl, colors["UVB"], peak_uvb_eff_std, peak_uvb_eff_prot, "UVB Peak"), 63 | (peak_uva_wl, colors["UVA"], peak_uva_eff_std, peak_uva_eff_prot, "UVA Peak"), 64 | ]: 65 | ax1.axvline(x=wl, color=color, linestyle="dotted", linewidth=2, label=lbl) 66 | ax1.scatter(wl, 1.0, color=color, zorder=5) 67 | ax1.text( 68 | wl + 2, 69 | y_annot, 70 | f"{eff_s:.4g} / {eff_p:.4g}", 71 | color=color, 72 | fontsize=10, 73 | ha="left", 74 | va="bottom", 75 | ) 76 | 77 | # 3.3) Primary axis styling & legend (resp + peaks) top-right 78 | ax1.set_xlabel("Wavelength (nm)") 79 | ax1.set_ylabel("Normalized Sensor Responsivity") 80 | ax1.set_ylim(0, 1.1) 81 | 82 | leg1 = ax1.legend(loc="upper right", bbox_to_anchor=(1, 0.8)) 83 | ax1.add_artist(leg1) 84 | 85 | # 3.4) Spectral-effectiveness curves on log scale & second legend 86 | ax2 = ax1.twinx() 87 | ax2.plot( 88 | w, 89 | eff_std, 90 | color="grey", 91 | linestyle="--", 92 | linewidth=2, 93 | alpha=0.6, 94 | label="SpecEff (standard)", 95 | ) 96 | ax2.plot( 97 | w, 98 | eff_prot, 99 | color="grey", 100 | linestyle="-.", 101 | linewidth=2, 102 | alpha=0.6, 103 | label="SpecEff (eyes protected)", 104 | ) 105 | ax2.set_ylabel("Relative Spectral Effectiveness (Log)") 106 | ax2.set_yscale("log") 107 | 108 | leg2 = ax2.legend(loc="upper right") 109 | ax2.add_artist(leg2) 110 | 111 | # 3.5) Final touches 112 | ax1.set_title("UV Responsivity and Spectral Effectiveness") 113 | plt.tight_layout() 114 | plt.show() 115 | -------------------------------------------------------------------------------- /scenes/uv_meter_scene_settings.cpp: -------------------------------------------------------------------------------- 1 | #include "uv_meter_app_i.hpp" 2 | #include "uv_meter_event.hpp" 3 | 4 | static const char* i2c_addresses[] = { 5 | [UVMeterI2CAddressAuto] = "Auto", 6 | [UVMeterI2CAddress74] = "0x74", 7 | [UVMeterI2CAddress75] = "0x75", 8 | [UVMeterI2CAddress76] = "0x76", 9 | [UVMeterI2CAddress77] = "0x77", 10 | }; 11 | 12 | static const char* units[] = { 13 | [UVMeterUnituW_cm_2] = "uW/cm2", 14 | [UVMeterUnitW_m_2] = "W/m2", 15 | [UVMeterUnitmW_m_2] = "mW/m2", 16 | }; 17 | 18 | static void i2c_address_change_callback(VariableItem* item) { 19 | UVMeterApp* app = static_cast(variable_item_get_context(item)); 20 | uint8_t index = variable_item_get_current_value_index(item); 21 | 22 | variable_item_set_current_value_text(item, i2c_addresses[index]); 23 | 24 | app->app_state->i2c_address = static_cast(index); 25 | } 26 | 27 | static void unit_change_callback(VariableItem* item) { 28 | UVMeterApp* app = static_cast(variable_item_get_context(item)); 29 | uint8_t index = variable_item_get_current_value_index(item); 30 | 31 | variable_item_set_current_value_text(item, units[index]); 32 | 33 | app->app_state->unit = static_cast(index); 34 | } 35 | 36 | static void enter_callback(void* context, uint32_t index) { 37 | auto* app = static_cast(context); 38 | switch(index) { 39 | // Update indices when adding new setting items 40 | case 2: 41 | scene_manager_handle_custom_event(app->scene_manager, UVMeterCustomEventSceneEnterHelp); 42 | break; 43 | case 3: 44 | scene_manager_handle_custom_event(app->scene_manager, UVMeterCustomEventSceneEnterAbout); 45 | break; 46 | default: 47 | break; 48 | } 49 | } 50 | 51 | void uv_meter_scene_settings_on_enter(void* context) { 52 | auto* app = static_cast(context); 53 | variable_item_list_reset(app->variable_item_list); 54 | VariableItem* item; 55 | 56 | item = variable_item_list_add( 57 | app->variable_item_list, 58 | "I2C Address", 59 | COUNT_OF(i2c_addresses), 60 | i2c_address_change_callback, 61 | app); 62 | variable_item_set_current_value_index(item, app->app_state->i2c_address); 63 | variable_item_set_current_value_text(item, i2c_addresses[app->app_state->i2c_address]); 64 | 65 | item = variable_item_list_add( 66 | app->variable_item_list, "Unit", COUNT_OF(units), unit_change_callback, app); 67 | variable_item_set_current_value_index(item, app->app_state->unit); 68 | variable_item_set_current_value_text(item, units[app->app_state->unit]); 69 | 70 | // Be aware when adding new items before "Help" to change index in `enter_callback()` 71 | variable_item_list_add(app->variable_item_list, "Help", 0, NULL, NULL); 72 | variable_item_list_add(app->variable_item_list, "About", 0, NULL, NULL); 73 | 74 | variable_item_list_set_enter_callback(app->variable_item_list, enter_callback, app); 75 | 76 | view_dispatcher_switch_to_view(app->view_dispatcher, UVMeterViewVariableItemList); 77 | } 78 | 79 | bool uv_meter_scene_settings_on_event(void* context, SceneManagerEvent event) { 80 | auto* app = static_cast(context); 81 | bool consumed = false; 82 | 83 | switch(event.type) { 84 | case SceneManagerEventTypeCustom: 85 | switch(event.event) { 86 | case UVMeterCustomEventSceneEnterHelp: 87 | scene_manager_next_scene(app->scene_manager, UVMeterSceneHelp); 88 | consumed = true; 89 | break; 90 | case UVMeterCustomEventSceneEnterAbout: 91 | scene_manager_next_scene(app->scene_manager, UVMeterSceneAbout); 92 | consumed = true; 93 | break; 94 | } 95 | break; 96 | default: 97 | break; 98 | } 99 | return consumed; 100 | } 101 | 102 | void uv_meter_scene_settings_on_exit(void* context) { 103 | auto* app = static_cast(context); 104 | variable_item_list_reset(app->variable_item_list); 105 | } -------------------------------------------------------------------------------- /scenes/uv_meter_scene_data.cpp: -------------------------------------------------------------------------------- 1 | #include "uv_meter_app_i.hpp" 2 | #include "uv_meter_event.hpp" 3 | 4 | #include 5 | 6 | /** 7 | * @brief Start a new measurement 8 | * 9 | * Initializes a new measurement and updates the measurement state. 10 | * 11 | * @param app_state Pointer to the shared app_state 12 | */ 13 | static void start_measurement(UVMeterAppState* app_state) { 14 | // Start measurement 15 | app_state->as7331.setMeasurementMode(MEASUREMENT_MODE_COMMAND); 16 | app_state->as7331.startMeasurement(); 17 | } 18 | 19 | /** 20 | * @brief Process the measurement results 21 | * 22 | * Retrieves the measurement results and updates the application state. 23 | * 24 | * @param app Pointer to the app 25 | */ 26 | static void process_measurement_results(UVMeterApp* app) { 27 | if(app->app_state->as7331.getResults(app->app_state->results, app->app_state->raw_results)) { 28 | FURI_LOG_D( 29 | "UV_Meter Data", 30 | "Irradiance UVA: %.2f µW/cm² UVB: %.2f µW/cm² UVC: %.2f µW/cm²", 31 | app->app_state->results.uv_a, 32 | app->app_state->results.uv_b, 33 | app->app_state->results.uv_c); 34 | uv_meter_data_set_results( 35 | app->uv_meter_data_view, &app->app_state->results, &app->app_state->raw_results); 36 | } else { 37 | FURI_LOG_E("UV_Meter Data", "Failed to get measurement results"); 38 | } 39 | } 40 | 41 | static void uv_meter_scene_data_enter_settings_callback(void* context) { 42 | furi_assert(context); 43 | auto* app = static_cast(context); 44 | view_dispatcher_send_custom_event(app->view_dispatcher, UVMeterCustomEventSceneEnterSettings); 45 | } 46 | 47 | void uv_meter_scene_data_on_enter(void* context) { 48 | auto* app = static_cast(context); 49 | 50 | uv_meter_update_from_sensor(app->uv_meter_data_view); 51 | uv_meter_data_set_unit(app->uv_meter_data_view, app->app_state->unit); 52 | 53 | uv_meter_data_set_enter_settings_callback( 54 | app->uv_meter_data_view, uv_meter_scene_data_enter_settings_callback, app); 55 | 56 | view_dispatcher_switch_to_view(app->view_dispatcher, UVMeterViewData); 57 | } 58 | 59 | bool uv_meter_scene_data_on_event(void* context, SceneManagerEvent event) { 60 | furi_assert(context); 61 | auto* app = static_cast(context); 62 | bool consumed = false; 63 | 64 | if(event.type == SceneManagerEventTypeCustom) { 65 | if(event.event == UVMeterCustomEventSceneEnterSettings) { 66 | scene_manager_next_scene(app->scene_manager, UVMeterSceneSettings); 67 | consumed = true; 68 | } 69 | 70 | } else if(event.type == SceneManagerEventTypeTick) { 71 | furi_mutex_acquire(app->app_state->as7331_mutex, FuriWaitForever); 72 | 73 | // Check if sensor got disconnected 74 | if(!app->app_state->as7331_initialized || !app->app_state->as7331.deviceReady()) { 75 | app->app_state->as7331_initialized = false; 76 | scene_manager_search_and_switch_to_previous_scene( 77 | app->scene_manager, UVMeterSceneWiring); 78 | } else { 79 | // Getting the status will put sensor into measurement state 80 | as7331_osr_status_reg_t status; 81 | app->app_state->as7331.getStatus(status); 82 | // Check if measurement is complete 83 | if(status.new_data) { 84 | process_measurement_results(app); 85 | start_measurement(app->app_state); 86 | } 87 | // This happens when measurement was interrupted by changing settings 88 | else if(status.osr.start_state != 1) { 89 | start_measurement(app->app_state); 90 | } 91 | // Handle overflows 92 | if(status.adc_overflow || status.result_overflow || status.out_conv_overflow) { 93 | FURI_LOG_E( 94 | "UV Meter Data", 95 | "Overflow detected! ADCOF (%d) MRESOF (%d) OUTCONVOF (%d)", 96 | status.adc_overflow, 97 | status.result_overflow, 98 | status.out_conv_overflow); 99 | } 100 | } 101 | consumed = true; 102 | furi_mutex_release(app->app_state->as7331_mutex); 103 | 104 | } else if(event.type == SceneManagerEventTypeBack) { 105 | // Always quit app in data scene 106 | scene_manager_stop(app->scene_manager); 107 | view_dispatcher_stop(app->view_dispatcher); 108 | consumed = true; 109 | } 110 | 111 | return consumed; 112 | } 113 | 114 | void uv_meter_scene_data_on_exit(void* context) { 115 | UNUSED(context); 116 | } 117 | -------------------------------------------------------------------------------- /uv_meter_app.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "uv_meter_app_i.hpp" 3 | 4 | // Forward events from View Dispatcher to Scene Manager 5 | static bool uv_meter_app_custom_callback(void* context, uint32_t custom_event) { 6 | furi_assert(context); 7 | auto* app = static_cast(context); 8 | return scene_manager_handle_custom_event(app->scene_manager, custom_event); 9 | } 10 | 11 | static bool uv_meter_app_back_event_callback(void* context) { 12 | furi_assert(context); 13 | auto* app = static_cast(context); 14 | bool value = scene_manager_handle_back_event(app->scene_manager); 15 | return value; 16 | } 17 | 18 | static void uv_meter_app_tick_callback(void* context) { 19 | furi_assert(context); 20 | auto* app = static_cast(context); 21 | scene_manager_handle_tick_event(app->scene_manager); 22 | } 23 | 24 | static UVMeterApp* uv_meter_app_alloc() { 25 | UVMeterApp* app = new UVMeterApp(); 26 | 27 | app->app_state = new UVMeterAppState(); 28 | app->app_state->as7331_mutex = furi_mutex_alloc(FuriMutexTypeNormal); 29 | app->app_state->results.uv_a = 0; 30 | app->app_state->results.uv_b = 0; 31 | app->app_state->results.uv_c = 0; 32 | app->app_state->raw_results.uv_a = 0; 33 | app->app_state->raw_results.uv_b = 0; 34 | app->app_state->raw_results.uv_c = 0; 35 | app->app_state->last_sensor_check_timestamp = 0; 36 | app->app_state->as7331_initialized = false; 37 | app->app_state->i2c_address = UVMeterI2CAddressAuto; 38 | app->app_state->unit = UVMeterUnituW_cm_2; 39 | 40 | app->scene_manager = scene_manager_alloc(&uv_meter_scene_handlers, app); 41 | app->view_dispatcher = view_dispatcher_alloc(); 42 | view_dispatcher_set_event_callback_context(app->view_dispatcher, app); 43 | view_dispatcher_set_custom_event_callback(app->view_dispatcher, uv_meter_app_custom_callback); 44 | view_dispatcher_set_navigation_event_callback( 45 | app->view_dispatcher, uv_meter_app_back_event_callback); 46 | view_dispatcher_set_tick_event_callback( 47 | app->view_dispatcher, uv_meter_app_tick_callback, furi_ms_to_ticks(150)); 48 | 49 | app->uv_meter_wiring_view = uv_meter_wiring_alloc(); 50 | view_dispatcher_add_view( 51 | app->view_dispatcher, 52 | UVMeterViewWiring, 53 | uv_meter_wiring_get_view(app->uv_meter_wiring_view)); 54 | app->variable_item_list = variable_item_list_alloc(); 55 | view_dispatcher_add_view( 56 | app->view_dispatcher, 57 | UVMeterViewVariableItemList, 58 | variable_item_list_get_view(app->variable_item_list)); 59 | app->uv_meter_data_view = uv_meter_data_alloc(); 60 | uv_meter_data_set_sensor( 61 | app->uv_meter_data_view, &app->app_state->as7331, app->app_state->as7331_mutex); 62 | view_dispatcher_add_view( 63 | app->view_dispatcher, UVMeterViewData, uv_meter_data_get_view(app->uv_meter_data_view)); 64 | app->widget = widget_alloc(); 65 | view_dispatcher_add_view( 66 | app->view_dispatcher, UVMeterViewWidget, widget_get_view(app->widget)); 67 | 68 | app->gui = static_cast(furi_record_open(RECORD_GUI)); 69 | view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); 70 | scene_manager_next_scene(app->scene_manager, UVMeterViewWiring); 71 | 72 | return app; 73 | } 74 | 75 | static void uv_meter_app_free(UVMeterApp* app) { 76 | furi_assert(app); 77 | furi_record_close(RECORD_GUI); 78 | view_dispatcher_remove_view(app->view_dispatcher, UVMeterViewWidget); 79 | widget_free(app->widget); 80 | view_dispatcher_remove_view(app->view_dispatcher, UVMeterViewData); 81 | uv_meter_data_free(app->uv_meter_data_view); 82 | view_dispatcher_remove_view(app->view_dispatcher, UVMeterViewVariableItemList); 83 | variable_item_list_free(app->variable_item_list); 84 | view_dispatcher_remove_view(app->view_dispatcher, UVMeterViewWiring); 85 | uv_meter_wiring_free(app->uv_meter_wiring_view); 86 | 87 | view_dispatcher_free(app->view_dispatcher); 88 | scene_manager_free(app->scene_manager); 89 | 90 | furi_mutex_free(app->app_state->as7331_mutex); 91 | delete app->app_state; 92 | delete app; 93 | } 94 | 95 | /** 96 | * @brief Main application entry point 97 | * 98 | * @param p Unused parameter 99 | * @return Exit code 100 | */ 101 | extern "C" int32_t uv_meter_app(void* p) { 102 | UNUSED(p); 103 | UVMeterApp* app = uv_meter_app_alloc(); 104 | 105 | view_dispatcher_run(app->view_dispatcher); 106 | 107 | // Power down the sensor before exiting 108 | if(app->app_state->as7331_initialized) { 109 | app->app_state->as7331.setPowerDown(true); 110 | app->app_state->as7331_initialized = false; 111 | } 112 | 113 | uv_meter_app_free(app); 114 | return 0; 115 | } 116 | -------------------------------------------------------------------------------- /magic_numbers/README.md: -------------------------------------------------------------------------------- 1 | # Magic Numbers Explained 2 | 3 | In the source code, within the [`uv_meter_data_calculate_effective_results()`](../views/uv_meter_data.cpp#L668) function, you'll see some seemingly arbitrary ("magic") numbers. They’re essential for converting raw UV sensor values into what's called "Effective Irradiance", which is then used to calculate the maximum daily exposure duration shown in the application: 4 | 5 | ```cpp 6 | UVMeterEffectiveResults 7 | uv_meter_data_calculate_effective_results(const AS7331::Results* results, bool eyes_protected) { 8 | // Weighted Spectral Effectiveness 9 | double w_spectral_eff_uv_a = 0.0002824; 10 | double w_spectral_eff_uv_b = 0.3814; 11 | double w_spectral_eff_uv_c = 0.6047; 12 | 13 | if(eyes_protected) { // 😎 14 | // w_spectral_eff_uv_a is the same 15 | w_spectral_eff_uv_b = 0.2009; 16 | w_spectral_eff_uv_c = 0.2547; 17 | } 18 | UVMeterEffectiveResults effective_results; 19 | // Effective Irradiance 20 | effective_results.uv_a_eff = results->uv_a * w_spectral_eff_uv_a; 21 | effective_results.uv_b_eff = results->uv_b * w_spectral_eff_uv_b; 22 | effective_results.uv_c_eff = results->uv_c * w_spectral_eff_uv_c; 23 | effective_results.uv_total_eff = 24 | effective_results.uv_a_eff + effective_results.uv_b_eff + effective_results.uv_c_eff; 25 | 26 | // Daily dose (seconds) based on the total effective irradiance 27 | double daily_dose = 0.003; // J/cm^2 28 | double uW_to_W = 1e-6; 29 | effective_results.t_max = daily_dose / (effective_results.uv_total_eff * uW_to_W); 30 | return effective_results; 31 | } 32 | ``` 33 | 34 | 35 | 36 | ## Why These Numbers? 37 | 38 | Before diving deeper, here's a quick explanation of what we're doing: 39 | 40 | 1. We start with raw UV-A, UV-B, and UV-C values from the sensor (in µW/cm²). 41 | 2. Multiply these raw values by the magic numbers to get "Effective Irradiance". 42 | 3. Sum these results to get a total effective irradiance. 43 | 4. Use a defined daily dose (`0.003 J/cm²`) to determine how long you'd need to be exposed at this irradiance level to reach that safe daily limit. 44 | 5. Finally, this calculated duration is displayed on the Flipper screen. 45 | 46 | 47 | 48 | ## But Where Did They Come From? 49 | 50 | These numbers are derived using two key documents: 51 | 52 | - The datasheet of the [AS7331 UV sensor](https://ams-osram.com/products/sensor-solutions/ambient-light-color-spectral-proximity-sensors/ams-as7331-spectral-uv-sensor). 53 | - The 2024 guidelines for [Threshold Limit Values (TLVs) and Biological Exposure Indices (BEIs)](https://en.wikipedia.org/wiki/Threshold_limit_value) published by the [American Conference of Governmental Industrial Hygienists (ACGIH)](https://en.wikipedia.org/wiki/American_Conference_of_Governmental_Industrial_Hygienists). Specifically, the chapter on "Ultraviolet Radiation", which, among other things, defines the daily exposure limit as `0.003 J/cm²`. Unfortunately, this document isn't freely accessible. 54 | 55 | 56 | 57 | ## Effective Irradiance Explained 58 | 59 | "Effective Irradiance" isn't just the raw UV sensor data–it's adjusted based on the human body's sensitivity to different UV wavelengths. Some wavelengths are more harmful than others, with the peak danger at 270 nm (in UV-C). At this peak, the "Relative Spectral Effectiveness" value is 1.0, and it decreases as wavelengths get shorter or longer. 60 | 61 | But here's the catch: the AS7331 sensor provides just one reading for each UV range: 62 | 63 | - UV-A (315–410 nm) 64 | - UV-B (280–315 nm) 65 | - UV-C (240–280 nm) 66 | 67 | However, the Relative Spectral Effectiveness values are listed individually every 5 nm. So, how do we pick a representative number? That’s exactly the puzzle I had to solve—and the magic numbers are my solution. 68 | 69 | 70 | 71 | ## The Method Behind the Magic 72 | 73 | ![](visualize_data.jpeg) 74 | 75 | My approach was to create a weighted spectral effectiveness curve by merging the sensor's Responsivity curves with the "Relative Spectral Effectiveness" curves, effectively combining both the sensor's sensitivity and the biological impact of each UV wavelength. 76 | 77 | The initial results seemed quite conservative, so I refined the approach by considering only the weighted effectiveness within clearly defined wavelength bands, ensuring there was no overlap between the UV channels. However, this meant that I also needed to account for the parts of the UV readings that fell outside those bands. To do this, I calculated a ratio: the total area under the original Responsivity curve compared to the area within the selected bands. This ratio is then applied to the UV readings to adjust for the excluded areas. 78 | 79 | The result of these steps is visualized in the two sets of plots below. There are two sets because the "Relative Spectral Effectiveness" varies depending on whether eyes are protected or not: 80 | 81 | ![](weighted_spectral_effectiveness_eyes_not_protected.jpeg) 82 | 83 | ![](weighted_spectral_effectiveness_eyes_protected.jpeg) 84 | 85 | At the top of each plot, the calculated Weighted Spectral Effectiveness value is displayed. These final numbers became the magic numbers in the code—hopefully providing a balanced and scientifically justified conversion from raw sensor data to Effective Irradiance. 86 | 87 | If you're curious or want to double-check my math, feel free to run the `weighted_spectral_effectiveness.py` script included here. 88 | 89 | 90 | 91 | ## Final Note and Sanity Check 92 | 93 | If you think the maximum daily exposure durations seem quite short, you're not alone. As a sanity check, I considered using the Spectral Effectiveness values at the sensor's peak response (you can see these in the first graph). However, this simpler approach results in similar, and often shorter, daily exposure durations, which suggests that my chosen method isn't far off. 94 | 95 | Finally, while these magic numbers might not be the only possible solution, I believe they are reasonable. If you have any insights or alternative approaches I may have missed, feel free to let me know. 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UV Meter 2 | 3 | A Flipper Zero application designed to measure ultraviolet (UV) radiation levels using the [AS7331](https://ams-osram.com/products/sensor-solutions/ambient-light-color-spectral-proximity-sensors/ams-as7331-spectral-uv-sensor) sensor. It supports individual measurements for UV-A, UV-B, and UV-C wavelengths. The easiest way to hook everything up is to use a breakout board, such as the one from [SparkFun](https://www.sparkfun.com/sparkfun-spectral-uv-sensor-as7331-qwiic.html). The application is available in the [Flipper App Catalog](https://lab.flipper.net/apps/uv_meter_as7331) and comes pre-installed on some custom firmware builds, such as the [Momentum Firmware](https://github.com/Next-Flip/Momentum-Firmware). For more behind-the-scenes info, check out the [post on my website](https://michaelbais.ch/projects/personal/uv_meter_for_flipper). 4 | 5 |

6 | AS7331 Sensor Connected to Flipper Zero with UV Meter Adapter PCB 7 | AS7331 Sensor Connected to Flipper Zero with cables 8 |

9 | 10 | 11 | 12 | ## Motivation 13 | 14 | Have you heard that sitting behind a window protects you from sunburn? Sounds good, right? Unless you do a bit of research and find it's only *kind of* true. While potentially some UV-B (the stuff that causes immediate sunburn) gets blocked, a lot of UV-A could still get through. And guess what? UV-A plays a role in developing melanoma—a deadly form of skin cancer. Now it almost sounds even *more* dangerous: you lose the immediate “sunburn feedback” yet still face a long-term risk. 15 | 16 | It gets even more complicated because it depends on the specific type of window and possible surface treatments; I have also heard that some car windows might be better in this regard. In the end, things seem less predictable, with more questions than before. And it doesn't stop there: do my sunglasses really work? Does my shirt actually protect me? How bad is it really in the shade or on a cloudy day? 17 | 18 | What we need is **data**. Being able to actually *measure* something can be surprisingly empowering. I found the AS7331 sensor, which can independently measure UV-A, UV-B, and UV-C—and this project was born. 19 | 20 | In general, it's surprising how low the maximum daily exposure durations actually are (based on the 2024 TLVs and BEIs by the [ACGIH](https://en.wikipedia.org/wiki/American_Conference_of_Governmental_Industrial_Hygienists)). Sure, compared to direct sunlight, you're better off behind a window, in the shade, or under clouds—but probably not *as much* as you'd expect. 21 | 22 | In some of my measurements, the safe daily exposure duration, for example, tripled—but when you're starting with just 3 minutes, tripling still leaves you under 10 minutes. My takeaway? I should protect my eyes and skin more than I once thought necessary. 23 | 24 | 25 | 26 | https://github.com/user-attachments/assets/cabb948c-9c79-4a1d-aab7-8789f0833f28 27 | 28 | 29 | 30 | ## Wiring 31 | 32 | Connect the AS7331 sensor to your Flipper Zero via I²C: 33 | 34 | ![Flipper Showing the Wiring Screen](images/wiring_screen.png) 35 | 36 | | Sensor Pin | Flipper Zero Pin | 37 | |------------|--------------------| 38 | | **SCL** | C0 [pin 16] | 39 | | **SDA** | C1 [pin 15] | 40 | | **3V3** | 3V3 [pin 9] | 41 | | **GND** | GND [pin 11 or 18] | 42 | 43 | By default, the application scans all possible I²C addresses for the sensor. However, you can manually set a specific address in the settings menu, accessible by pressing the **Enter** button. 44 | 45 | 46 | 47 | ## Usage 48 | 49 | Once connected, the application automatically displays real-time UV measurements. The main screen shows individual UV-A, UV-B, and UV-C readings. Beneath the numbers you see the currently used unit (µW/cm², W/m² or mW/m²). 50 | 51 | ![Data Screen with Explanations](images/main_screen_explanation.jpg) 52 | 53 | Next to each UV reading is a small meter indicating the raw sensor value. If this value is too high or too low, a warning symbol will appear, signaling potential sensor overexposure or underexposure. This condition will probably result in unreliable measurements. Adjusting the **Gain** and **Exposure Time** settings (similar to camera ISO and shutter speed) at the bottom of the screen can help correct this. 54 | 55 | In addition to direct sensor measurements, the application provides an interpretation of this irradiance data on human safety, displayed on the right side of the screen. The time (in minutes) indicates the recommended maximum daily exposure duration during an 8-hour work shift, based on the 2024 Threshold Limit Values (TLVs) and Biological Exposure Indices (BEIs) published by the [American Conference of Governmental Industrial Hygienists (ACGIH)](https://en.wikipedia.org/wiki/American_Conference_of_Governmental_Industrial_Hygienists). According to ACGIH, the TLVs referenced apply only to incoherent UV radiation; coherent UV radiation such as that emitted by lasers is handled differently. 56 | 57 | Since UV radiation poses greater risks to the eyes, the app includes a setting to specify whether your eyes are protected. This adjusts the maximum daily exposure duration according to ACGIH recommendations. 58 | 59 | The displayed percentages indicate how much each UV type (UV-A, UV-B, UV-C) contributes to the maximum daily exposure. This helps illustrate that higher sensor readings don't necessarily mean a greater health risk; for example, even though UV-A sensor values are typically higher, they usually contribute less to the recommended maximum exposure limit compared to UV-B or UV-C. 60 | 61 | When following the maximum daily exposure duration, the TLV/BEI guidelines ensure: 62 | 63 | > “[...] nearly all healthy workers may be repeatedly exposed without acute adverse health effects such as erythema and photokeratitis.” 64 | 65 | 66 | 67 | ## Magic Numbers 68 | 69 | In the source code, specifically the [`uv_meter_data_calculate_effective_results()`](views/uv_meter_data.cpp#L668) function, you'll find some numbers that might look mysterious ("magic numbers"). They're used to calculate the maximum daily UV exposure duration based on sensor readings: 70 | 71 | ```cpp 72 | // Weighted Spectral Effectiveness 73 | double w_spectral_eff_uv_a = 0.0002824; 74 | double w_spectral_eff_uv_b = 0.3814; 75 | double w_spectral_eff_uv_c = 0.6047; 76 | 77 | if(eyes_protected) { // 😎 78 | // w_spectral_eff_uv_a is the same 79 | w_spectral_eff_uv_b = 0.2009; 80 | w_spectral_eff_uv_c = 0.2547; 81 | } 82 | ``` 83 | 84 | You might wonder, "Where do these numbers come from?" Good question! To uncover the full story behind these values, check out the detailed explanation in the [Magic Numbers documentation](magic_numbers/README.md). 85 | 86 | 87 | 88 | ## Adapter PCB 89 | 90 | As a final touch, I designed a tiny adapter PCB in KiCad that lets me connect the breakout board to the Flipper — no more cable fiddling required. (One might even call its color ~~dark purple~~ ultraviolet.) The front only shows the application icon and minimal labeling to help you avoid plugging things in the wrong way. On the back, the labels show which Flipper pins connect to which sensor pins. See the [adapter_pcb](adapter_pcb) directory for the KiCad and Gerber files. 91 | 92 | ![Render of the UV Meter Adapter PCB](images/adapter_pcb.jpg) 93 | 94 | 95 | 96 | ## Disclaimer 97 | 98 | This application is provided for informational purposes only and should not be used as a sole basis for safety-critical decisions. Always follow official guidelines, regulations, and professional advice regarding UV exposure. The developer assumes no responsibility for any damages, injuries, or consequences arising from decisions or actions based on the information provided by this application. 99 | -------------------------------------------------------------------------------- /magic_numbers/weighted_spectral_effectiveness.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | # ----------------------------------------------------- 6 | # 1) Read and prepare data 7 | # ----------------------------------------------------- 8 | file_path = "./uv_responsivity_and_spectral_effectiveness_data.csv" 9 | df = pd.read_csv(file_path) 10 | 11 | w = df["Wavelength (nm)"].values 12 | uvc = df["UVC"].values 13 | uvb = df["UVB"].values 14 | uva = df["UVA"].values 15 | # eff = df["Relative Spectral Effectiveness"].values 16 | eff = df["Relative Spectral Effectiveness (eyes protected)"].values 17 | 18 | # We have two cutoffs (rounded to data points): 19 | cutoff_uvc_uvb = 280 # exact: 280.7 20 | cutoff_uvb_uva = 315 # exact: 315.5 21 | 22 | # Calculate "effective" curves = Responsivity * Spectral Effectiveness 23 | uvc_eff = uvc * eff 24 | uvb_eff = uvb * eff 25 | uva_eff = uva * eff 26 | 27 | 28 | # ----------------------------------------------------- 29 | # 2) Weighted spectral effectiveness (in-band only) 30 | # ----------------------------------------------------- 31 | def compute_weighted_eff_in_band(wl, resp, sp_eff, band_min, band_max): 32 | """ 33 | Weighted spectral effectiveness = sum(resp * sp_eff) / sum(resp), 34 | but only for band_min <= wl < band_max (and resp>0). 35 | (using Responsivity as the weight) 36 | """ 37 | mask = (wl >= band_min) & (wl < band_max) & (resp > 0) 38 | if mask.sum() == 0: 39 | return 0.0 40 | return (resp[mask] * sp_eff[mask]).sum() / resp[mask].sum() 41 | 42 | 43 | wl_min = w[0] 44 | wl_max = w[-1] 45 | 46 | uvc_w_eff = compute_weighted_eff_in_band(w, uvc, eff, wl_min, cutoff_uvc_uvb) 47 | uvb_w_eff = compute_weighted_eff_in_band(w, uvb, eff, cutoff_uvc_uvb, cutoff_uvb_uva) 48 | uva_w_eff = compute_weighted_eff_in_band(w, uva, eff, cutoff_uvb_uva, wl_max) 49 | 50 | 51 | # ----------------------------------------------------- 52 | # 2.5) Compute Area Ratios and Correct Weighted Spectral Effectiveness 53 | # ----------------------------------------------------- 54 | def compute_area_ratio(wl, curve, band_min, band_max): 55 | """ 56 | Compute the ratio of the area under the curve in the band [band_min, band_max) 57 | to the total area under the curve. 58 | """ 59 | total_area = np.trapezoid(curve, wl) 60 | band_mask = (wl >= band_min) & (wl <= band_max) 61 | band_area = np.trapezoid(curve[band_mask], wl[band_mask]) 62 | return band_area / total_area if total_area > 0 else 0.0 63 | 64 | 65 | # Compute area ratios for the Responsivity curves 66 | uvc_area_ratio = compute_area_ratio(w, uvc, wl_min, cutoff_uvc_uvb) 67 | uvb_area_ratio = compute_area_ratio(w, uvb, cutoff_uvc_uvb, cutoff_uvb_uva) 68 | uva_area_ratio = compute_area_ratio(w, uva, cutoff_uvb_uva, wl_max) 69 | 70 | # Correct Weighted Spectral Effectiveness 71 | uvc_w_eff = uvc_w_eff * uvc_area_ratio 72 | uvb_w_eff = uvb_w_eff * uvb_area_ratio 73 | uva_w_eff = uva_w_eff * uva_area_ratio 74 | 75 | 76 | # ----------------------------------------------------- 77 | # 3) In-band peaks 78 | # ----------------------------------------------------- 79 | def find_in_band_peak(wl, y, band_min, band_max): 80 | """ 81 | Return (peak_wl, peak_val) for the maximum of y where band_min <= wl <= band_max. 82 | """ 83 | mask = (wl >= band_min) & (wl < band_max) 84 | if not np.any(mask): 85 | return None, None 86 | idx_max = np.argmax(y[mask]) 87 | peak_val = y[mask][idx_max] 88 | peak_wl = wl[mask][idx_max] 89 | return peak_wl, peak_val 90 | 91 | 92 | uvc_peak_wl, uvc_peak_val = find_in_band_peak(w, uvc_eff, wl_min, cutoff_uvc_uvb) 93 | uvb_peak_wl, uvb_peak_val = find_in_band_peak( 94 | w, uvb_eff, cutoff_uvc_uvb, cutoff_uvb_uva 95 | ) 96 | uva_peak_wl, uva_peak_val = find_in_band_peak(w, uva_eff, cutoff_uvb_uva, wl_max) 97 | 98 | 99 | # ----------------------------------------------------- 100 | # 4) Plotting Helpers: Split in-band vs. out-of-band 101 | # We do it by creating arrays with NaN outside the band 102 | # so matplotlib doesn't draw lines across boundaries. 103 | # ----------------------------------------------------- 104 | def band_split(wl, arr, band_min, band_max): 105 | """ 106 | Return two arrays: in-band (NaN outside) and out-of-band (NaN inside), 107 | so we can plot them separately and avoid bridging lines across cutoff edges. 108 | """ 109 | in_mask = (wl >= band_min) & (wl <= band_max) 110 | out_mask = (wl <= band_min) | (wl >= band_max) # ~in_mask 111 | 112 | in_array = np.where(in_mask, arr, np.nan) 113 | out_array = np.where(out_mask, arr, np.nan) 114 | return in_array, out_array 115 | 116 | 117 | # We'll also define a small function to plot the peak marker 118 | def add_peak_marker(ax, peak_wl, peak_val, color): 119 | if peak_wl is not None and peak_val is not None: 120 | ax.scatter(peak_wl, peak_val, color=color, zorder=5) 121 | ax.text( 122 | peak_wl + 2, 123 | peak_val, 124 | f"{peak_val:.4g} @{peak_wl:.0f}nm", 125 | color=color, 126 | fontsize=9, 127 | ha="left", 128 | va="bottom", 129 | ) 130 | 131 | 132 | # ----------------------------------------------------- 133 | # 6) Create figure with 3 subplots 134 | # ----------------------------------------------------- 135 | colors = { 136 | "UVC": "#4d4d4d", # darker/grey 137 | "UVB": "#8a2be2", # violet/purple 138 | "UVA": "#0b66f2", # pleasing blue 139 | } 140 | 141 | fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 12), sharex=True) 142 | plt.subplots_adjust(hspace=0.1) 143 | 144 | 145 | # A helper to plot everything for a given subplot 146 | def plot_subplot( 147 | ax, 148 | w, 149 | resp, 150 | eff, 151 | eff_curve, 152 | band_min, 153 | band_max, 154 | color, 155 | label, 156 | weighted_eff, 157 | peak_wl, 158 | peak_val, 159 | ): 160 | """ 161 | Plots: 162 | - background (responsivity in linear scale, spectral eff in log scale) 163 | - in-band and out-of-band lines for 'eff_curve' (which is resp*eff) 164 | - vertical cutoffs 165 | - weighted efficiency text 166 | - peak marker 167 | """ 168 | 169 | # Split in-band vs out-of-band 170 | main_in, main_out = band_split(w, eff_curve, band_min, band_max) 171 | p1 = ax.plot( 172 | w, main_out, color=color, alpha=0.4, linewidth=1, label=f"{label} - out" 173 | ) 174 | p2 = ax.plot(w, main_in, color=color, alpha=1.0, linewidth=2, label=f"{label} - in") 175 | ax.set_ylim(0, 1.1 * np.max(eff_curve)) 176 | 177 | # Background plots 178 | ax_resp = ax.twinx() 179 | ax_eff = ax.twinx() 180 | ax_eff.spines.right.set_position(("axes", 1.1)) 181 | ax_eff.set_yscale("log", nonpositive="clip") 182 | 183 | p3 = ax_resp.plot(w, resp, color="grey", linestyle="-", alpha=0.5, label="Resp") 184 | p4 = ax_eff.plot( 185 | w, 186 | eff, 187 | color="grey", 188 | linestyle="--", 189 | alpha=0.6, 190 | label="SpecEff", 191 | ) 192 | ax_resp.set_ylabel("Normalized Sensor Responsivity") 193 | ax_eff.set_ylabel("Relative Spectral Effectiveness") 194 | 195 | # Add cutoff lines 196 | ax.axvline(x=cutoff_uvc_uvb, color="black", linestyle="dotted", alpha=0.5) 197 | ax.axvline(x=cutoff_uvb_uva, color="black", linestyle="dotted", alpha=0.5) 198 | 199 | # Weighted Spectral Effectiveness text annotation 200 | text_str = f"Weighted Spectral Effectiveness (in band): {weighted_eff:.4g}" 201 | ax.text( 202 | 0.00, 203 | 1.0055, 204 | text_str, 205 | transform=ax.transAxes, 206 | color=color, 207 | fontsize=10, 208 | ha="left", 209 | va="bottom", 210 | ) 211 | 212 | # Mark the peak 213 | add_peak_marker(ax, peak_wl, peak_val, color) 214 | 215 | # Final styling 216 | ax.legend(loc="upper right", handles=[p1[0], p2[0], p3[0], p4[0]]) 217 | ax.set_ylabel(f"{label}") 218 | 219 | 220 | # --------------------- Subplot for UVC -------------------------- 221 | plot_subplot( 222 | ax1, 223 | w, 224 | uvc, 225 | eff, 226 | uvc_eff, 227 | band_min=w[0], 228 | band_max=cutoff_uvc_uvb, 229 | color=colors["UVC"], 230 | label="Resp (UVC) * SpecEff", 231 | weighted_eff=uvc_w_eff, 232 | peak_wl=uvc_peak_wl, 233 | peak_val=uvc_peak_val, 234 | ) 235 | 236 | # --------------------- Subplot for UVB -------------------------- 237 | plot_subplot( 238 | ax2, 239 | w, 240 | uvb, 241 | eff, 242 | uvb_eff, 243 | band_min=cutoff_uvc_uvb, 244 | band_max=cutoff_uvb_uva, 245 | color=colors["UVB"], 246 | label="Resp (UVB) * SpecEff", 247 | weighted_eff=uvb_w_eff, 248 | peak_wl=uvb_peak_wl, 249 | peak_val=uvb_peak_val, 250 | ) 251 | 252 | # --------------------- Subplot for UVA -------------------------- 253 | plot_subplot( 254 | ax3, 255 | w, 256 | uva, 257 | eff, 258 | uva_eff, 259 | band_min=cutoff_uvb_uva, 260 | band_max=w[-1], 261 | color=colors["UVA"], 262 | label="Resp (UVA) * SpecEff", 263 | weighted_eff=uva_w_eff, 264 | peak_wl=uva_peak_wl, 265 | peak_val=uva_peak_val, 266 | ) 267 | 268 | ax3.set_xlabel("Wavelength (nm)") 269 | 270 | plt.tight_layout() 271 | plt.show() 272 | -------------------------------------------------------------------------------- /adapter_pcb/flipper_as7331_adapter/flipper_as7331_adapter.kicad_pro: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "3dviewports": [], 4 | "design_settings": { 5 | "defaults": { 6 | "apply_defaults_to_fp_fields": false, 7 | "apply_defaults_to_fp_shapes": false, 8 | "apply_defaults_to_fp_text": false, 9 | "board_outline_line_width": 0.05, 10 | "copper_line_width": 0.2, 11 | "copper_text_italic": false, 12 | "copper_text_size_h": 1.5, 13 | "copper_text_size_v": 1.5, 14 | "copper_text_thickness": 0.3, 15 | "copper_text_upright": false, 16 | "courtyard_line_width": 0.05, 17 | "dimension_precision": 4, 18 | "dimension_units": 3, 19 | "dimensions": { 20 | "arrow_length": 1270000, 21 | "extension_offset": 500000, 22 | "keep_text_aligned": true, 23 | "suppress_zeroes": true, 24 | "text_position": 0, 25 | "units_format": 0 26 | }, 27 | "fab_line_width": 0.1, 28 | "fab_text_italic": false, 29 | "fab_text_size_h": 1.0, 30 | "fab_text_size_v": 1.0, 31 | "fab_text_thickness": 0.15, 32 | "fab_text_upright": false, 33 | "other_line_width": 0.1, 34 | "other_text_italic": false, 35 | "other_text_size_h": 1.0, 36 | "other_text_size_v": 1.0, 37 | "other_text_thickness": 0.15, 38 | "other_text_upright": false, 39 | "pads": { 40 | "drill": 0.8, 41 | "height": 1.27, 42 | "width": 2.54 43 | }, 44 | "silk_line_width": 0.1, 45 | "silk_text_italic": false, 46 | "silk_text_size_h": 1.0, 47 | "silk_text_size_v": 1.0, 48 | "silk_text_thickness": 0.1, 49 | "silk_text_upright": false, 50 | "zones": { 51 | "min_clearance": 0.5 52 | } 53 | }, 54 | "diff_pair_dimensions": [ 55 | { 56 | "gap": 0.0, 57 | "via_gap": 0.0, 58 | "width": 0.0 59 | } 60 | ], 61 | "drc_exclusions": [], 62 | "meta": { 63 | "version": 2 64 | }, 65 | "rule_severities": { 66 | "annular_width": "error", 67 | "clearance": "error", 68 | "connection_width": "warning", 69 | "copper_edge_clearance": "error", 70 | "copper_sliver": "warning", 71 | "courtyards_overlap": "error", 72 | "creepage": "error", 73 | "diff_pair_gap_out_of_range": "error", 74 | "diff_pair_uncoupled_length_too_long": "error", 75 | "drill_out_of_range": "error", 76 | "duplicate_footprints": "warning", 77 | "extra_footprint": "warning", 78 | "footprint": "error", 79 | "footprint_filters_mismatch": "ignore", 80 | "footprint_symbol_mismatch": "warning", 81 | "footprint_type_mismatch": "ignore", 82 | "hole_clearance": "error", 83 | "hole_to_hole": "warning", 84 | "holes_co_located": "warning", 85 | "invalid_outline": "error", 86 | "isolated_copper": "warning", 87 | "item_on_disabled_layer": "error", 88 | "items_not_allowed": "error", 89 | "length_out_of_range": "error", 90 | "lib_footprint_issues": "warning", 91 | "lib_footprint_mismatch": "warning", 92 | "malformed_courtyard": "error", 93 | "microvia_drill_out_of_range": "error", 94 | "mirrored_text_on_front_layer": "warning", 95 | "missing_courtyard": "ignore", 96 | "missing_footprint": "warning", 97 | "net_conflict": "warning", 98 | "nonmirrored_text_on_back_layer": "warning", 99 | "npth_inside_courtyard": "ignore", 100 | "padstack": "warning", 101 | "pth_inside_courtyard": "ignore", 102 | "shorting_items": "error", 103 | "silk_edge_clearance": "warning", 104 | "silk_over_copper": "warning", 105 | "silk_overlap": "warning", 106 | "skew_out_of_range": "error", 107 | "solder_mask_bridge": "error", 108 | "starved_thermal": "error", 109 | "text_height": "warning", 110 | "text_on_edge_cuts": "error", 111 | "text_thickness": "warning", 112 | "through_hole_pad_without_hole": "error", 113 | "too_many_vias": "error", 114 | "track_angle": "error", 115 | "track_dangling": "warning", 116 | "track_segment_length": "error", 117 | "track_width": "error", 118 | "tracks_crossing": "error", 119 | "unconnected_items": "error", 120 | "unresolved_variable": "error", 121 | "via_dangling": "warning", 122 | "zones_intersect": "error" 123 | }, 124 | "rules": { 125 | "max_error": 0.005, 126 | "min_clearance": 0.0, 127 | "min_connection": 0.0, 128 | "min_copper_edge_clearance": 0.5, 129 | "min_groove_width": 0.0, 130 | "min_hole_clearance": 0.25, 131 | "min_hole_to_hole": 0.25, 132 | "min_microvia_diameter": 0.2, 133 | "min_microvia_drill": 0.1, 134 | "min_resolved_spokes": 2, 135 | "min_silk_clearance": 0.0, 136 | "min_text_height": 0.8, 137 | "min_text_thickness": 0.08, 138 | "min_through_hole_diameter": 0.3, 139 | "min_track_width": 0.0, 140 | "min_via_annular_width": 0.1, 141 | "min_via_diameter": 0.5, 142 | "solder_mask_to_copper_clearance": 0.0, 143 | "use_height_for_length_calcs": true 144 | }, 145 | "teardrop_options": [ 146 | { 147 | "td_onpthpad": true, 148 | "td_onroundshapesonly": false, 149 | "td_onsmdpad": true, 150 | "td_ontrackend": false, 151 | "td_onvia": true 152 | } 153 | ], 154 | "teardrop_parameters": [ 155 | { 156 | "td_allow_use_two_tracks": true, 157 | "td_curve_segcount": 0, 158 | "td_height_ratio": 1.0, 159 | "td_length_ratio": 0.5, 160 | "td_maxheight": 2.0, 161 | "td_maxlen": 1.0, 162 | "td_on_pad_in_zone": false, 163 | "td_target_name": "td_round_shape", 164 | "td_width_to_size_filter_ratio": 0.9 165 | }, 166 | { 167 | "td_allow_use_two_tracks": true, 168 | "td_curve_segcount": 0, 169 | "td_height_ratio": 1.0, 170 | "td_length_ratio": 0.5, 171 | "td_maxheight": 2.0, 172 | "td_maxlen": 1.0, 173 | "td_on_pad_in_zone": false, 174 | "td_target_name": "td_rect_shape", 175 | "td_width_to_size_filter_ratio": 0.9 176 | }, 177 | { 178 | "td_allow_use_two_tracks": true, 179 | "td_curve_segcount": 0, 180 | "td_height_ratio": 1.0, 181 | "td_length_ratio": 0.5, 182 | "td_maxheight": 2.0, 183 | "td_maxlen": 1.0, 184 | "td_on_pad_in_zone": false, 185 | "td_target_name": "td_track_end", 186 | "td_width_to_size_filter_ratio": 0.9 187 | } 188 | ], 189 | "track_widths": [ 190 | 0.0, 191 | 0.4 192 | ], 193 | "tuning_pattern_settings": { 194 | "diff_pair_defaults": { 195 | "corner_radius_percentage": 80, 196 | "corner_style": 1, 197 | "max_amplitude": 1.0, 198 | "min_amplitude": 0.2, 199 | "single_sided": false, 200 | "spacing": 1.0 201 | }, 202 | "diff_pair_skew_defaults": { 203 | "corner_radius_percentage": 80, 204 | "corner_style": 1, 205 | "max_amplitude": 1.0, 206 | "min_amplitude": 0.2, 207 | "single_sided": false, 208 | "spacing": 0.6 209 | }, 210 | "single_track_defaults": { 211 | "corner_radius_percentage": 80, 212 | "corner_style": 1, 213 | "max_amplitude": 1.0, 214 | "min_amplitude": 0.2, 215 | "single_sided": false, 216 | "spacing": 0.6 217 | } 218 | }, 219 | "via_dimensions": [ 220 | { 221 | "diameter": 0.0, 222 | "drill": 0.0 223 | } 224 | ], 225 | "zones_allow_external_fillets": false 226 | }, 227 | "ipc2581": { 228 | "dist": "", 229 | "distpn": "", 230 | "internal_id": "", 231 | "mfg": "", 232 | "mpn": "" 233 | }, 234 | "layer_pairs": [], 235 | "layer_presets": [], 236 | "viewports": [] 237 | }, 238 | "boards": [], 239 | "cvpcb": { 240 | "equivalence_files": [] 241 | }, 242 | "erc": { 243 | "erc_exclusions": [], 244 | "meta": { 245 | "version": 0 246 | }, 247 | "pin_map": [ 248 | [ 249 | 0, 250 | 0, 251 | 0, 252 | 0, 253 | 0, 254 | 0, 255 | 1, 256 | 0, 257 | 0, 258 | 0, 259 | 0, 260 | 2 261 | ], 262 | [ 263 | 0, 264 | 2, 265 | 0, 266 | 1, 267 | 0, 268 | 0, 269 | 1, 270 | 0, 271 | 2, 272 | 2, 273 | 2, 274 | 2 275 | ], 276 | [ 277 | 0, 278 | 0, 279 | 0, 280 | 0, 281 | 0, 282 | 0, 283 | 1, 284 | 0, 285 | 1, 286 | 0, 287 | 1, 288 | 2 289 | ], 290 | [ 291 | 0, 292 | 1, 293 | 0, 294 | 0, 295 | 0, 296 | 0, 297 | 1, 298 | 1, 299 | 2, 300 | 1, 301 | 1, 302 | 2 303 | ], 304 | [ 305 | 0, 306 | 0, 307 | 0, 308 | 0, 309 | 0, 310 | 0, 311 | 1, 312 | 0, 313 | 0, 314 | 0, 315 | 0, 316 | 2 317 | ], 318 | [ 319 | 0, 320 | 0, 321 | 0, 322 | 0, 323 | 0, 324 | 0, 325 | 0, 326 | 0, 327 | 0, 328 | 0, 329 | 0, 330 | 2 331 | ], 332 | [ 333 | 1, 334 | 1, 335 | 1, 336 | 1, 337 | 1, 338 | 0, 339 | 1, 340 | 1, 341 | 1, 342 | 1, 343 | 1, 344 | 2 345 | ], 346 | [ 347 | 0, 348 | 0, 349 | 0, 350 | 1, 351 | 0, 352 | 0, 353 | 1, 354 | 0, 355 | 0, 356 | 0, 357 | 0, 358 | 2 359 | ], 360 | [ 361 | 0, 362 | 2, 363 | 1, 364 | 2, 365 | 0, 366 | 0, 367 | 1, 368 | 0, 369 | 2, 370 | 2, 371 | 2, 372 | 2 373 | ], 374 | [ 375 | 0, 376 | 2, 377 | 0, 378 | 1, 379 | 0, 380 | 0, 381 | 1, 382 | 0, 383 | 2, 384 | 0, 385 | 0, 386 | 2 387 | ], 388 | [ 389 | 0, 390 | 2, 391 | 1, 392 | 1, 393 | 0, 394 | 0, 395 | 1, 396 | 0, 397 | 2, 398 | 0, 399 | 0, 400 | 2 401 | ], 402 | [ 403 | 2, 404 | 2, 405 | 2, 406 | 2, 407 | 2, 408 | 2, 409 | 2, 410 | 2, 411 | 2, 412 | 2, 413 | 2, 414 | 2 415 | ] 416 | ], 417 | "rule_severities": { 418 | "bus_definition_conflict": "error", 419 | "bus_entry_needed": "error", 420 | "bus_to_bus_conflict": "error", 421 | "bus_to_net_conflict": "error", 422 | "different_unit_footprint": "error", 423 | "different_unit_net": "error", 424 | "duplicate_reference": "error", 425 | "duplicate_sheet_names": "error", 426 | "endpoint_off_grid": "warning", 427 | "extra_units": "error", 428 | "footprint_filter": "ignore", 429 | "footprint_link_issues": "warning", 430 | "four_way_junction": "ignore", 431 | "global_label_dangling": "warning", 432 | "hier_label_mismatch": "error", 433 | "label_dangling": "error", 434 | "label_multiple_wires": "warning", 435 | "lib_symbol_issues": "warning", 436 | "lib_symbol_mismatch": "warning", 437 | "missing_bidi_pin": "warning", 438 | "missing_input_pin": "warning", 439 | "missing_power_pin": "error", 440 | "missing_unit": "warning", 441 | "multiple_net_names": "warning", 442 | "net_not_bus_member": "warning", 443 | "no_connect_connected": "warning", 444 | "no_connect_dangling": "warning", 445 | "pin_not_connected": "error", 446 | "pin_not_driven": "error", 447 | "pin_to_pin": "warning", 448 | "power_pin_not_driven": "error", 449 | "same_local_global_label": "warning", 450 | "similar_label_and_power": "warning", 451 | "similar_labels": "warning", 452 | "similar_power": "warning", 453 | "simulation_model_issue": "ignore", 454 | "single_global_label": "ignore", 455 | "unannotated": "error", 456 | "unconnected_wire_endpoint": "warning", 457 | "unit_value_mismatch": "error", 458 | "unresolved_variable": "error", 459 | "wire_dangling": "error" 460 | } 461 | }, 462 | "libraries": { 463 | "pinned_footprint_libs": [], 464 | "pinned_symbol_libs": [] 465 | }, 466 | "meta": { 467 | "filename": "flipper_as7331_adapter.kicad_pro", 468 | "version": 3 469 | }, 470 | "net_settings": { 471 | "classes": [ 472 | { 473 | "bus_width": 12, 474 | "clearance": 0.2, 475 | "diff_pair_gap": 0.25, 476 | "diff_pair_via_gap": 0.25, 477 | "diff_pair_width": 0.2, 478 | "line_style": 0, 479 | "microvia_diameter": 0.3, 480 | "microvia_drill": 0.1, 481 | "name": "Default", 482 | "pcb_color": "rgba(0, 0, 0, 0.000)", 483 | "priority": 2147483647, 484 | "schematic_color": "rgba(0, 0, 0, 0.000)", 485 | "track_width": 0.2, 486 | "via_diameter": 0.6, 487 | "via_drill": 0.3, 488 | "wire_width": 6 489 | } 490 | ], 491 | "meta": { 492 | "version": 4 493 | }, 494 | "net_colors": null, 495 | "netclass_assignments": null, 496 | "netclass_patterns": [] 497 | }, 498 | "pcbnew": { 499 | "last_paths": { 500 | "gencad": "", 501 | "idf": "", 502 | "netlist": "", 503 | "plot": "../gerber_v1.0/", 504 | "pos_files": "", 505 | "specctra_dsn": "", 506 | "step": "", 507 | "svg": "", 508 | "vrml": "" 509 | }, 510 | "page_layout_descr_file": "" 511 | }, 512 | "schematic": { 513 | "annotate_start_num": 0, 514 | "bom_export_filename": "${PROJECTNAME}.csv", 515 | "bom_fmt_presets": [], 516 | "bom_fmt_settings": { 517 | "field_delimiter": ",", 518 | "keep_line_breaks": false, 519 | "keep_tabs": false, 520 | "name": "CSV", 521 | "ref_delimiter": ",", 522 | "ref_range_delimiter": "", 523 | "string_delimiter": "\"" 524 | }, 525 | "bom_presets": [], 526 | "bom_settings": { 527 | "exclude_dnp": false, 528 | "fields_ordered": [ 529 | { 530 | "group_by": false, 531 | "label": "Reference", 532 | "name": "Reference", 533 | "show": true 534 | }, 535 | { 536 | "group_by": false, 537 | "label": "Qty", 538 | "name": "${QUANTITY}", 539 | "show": true 540 | }, 541 | { 542 | "group_by": true, 543 | "label": "Value", 544 | "name": "Value", 545 | "show": true 546 | }, 547 | { 548 | "group_by": true, 549 | "label": "DNP", 550 | "name": "${DNP}", 551 | "show": true 552 | }, 553 | { 554 | "group_by": true, 555 | "label": "Exclude from BOM", 556 | "name": "${EXCLUDE_FROM_BOM}", 557 | "show": true 558 | }, 559 | { 560 | "group_by": true, 561 | "label": "Exclude from Board", 562 | "name": "${EXCLUDE_FROM_BOARD}", 563 | "show": true 564 | }, 565 | { 566 | "group_by": true, 567 | "label": "Footprint", 568 | "name": "Footprint", 569 | "show": true 570 | }, 571 | { 572 | "group_by": false, 573 | "label": "Datasheet", 574 | "name": "Datasheet", 575 | "show": true 576 | } 577 | ], 578 | "filter_string": "", 579 | "group_symbols": true, 580 | "include_excluded_from_bom": true, 581 | "name": "Default Editing", 582 | "sort_asc": true, 583 | "sort_field": "Reference" 584 | }, 585 | "connection_grid_size": 50.0, 586 | "drawing": { 587 | "dashed_lines_dash_length_ratio": 12.0, 588 | "dashed_lines_gap_length_ratio": 3.0, 589 | "default_line_thickness": 6.0, 590 | "default_text_size": 50.0, 591 | "field_names": [], 592 | "intersheets_ref_own_page": false, 593 | "intersheets_ref_prefix": "", 594 | "intersheets_ref_short": false, 595 | "intersheets_ref_show": false, 596 | "intersheets_ref_suffix": "", 597 | "junction_size_choice": 3, 598 | "label_size_ratio": 0.375, 599 | "operating_point_overlay_i_precision": 3, 600 | "operating_point_overlay_i_range": "~A", 601 | "operating_point_overlay_v_precision": 3, 602 | "operating_point_overlay_v_range": "~V", 603 | "overbar_offset_ratio": 1.23, 604 | "pin_symbol_size": 25.0, 605 | "text_offset_ratio": 0.15 606 | }, 607 | "legacy_lib_dir": "", 608 | "legacy_lib_list": [], 609 | "meta": { 610 | "version": 1 611 | }, 612 | "net_format_name": "", 613 | "page_layout_descr_file": "", 614 | "plot_directory": "", 615 | "space_save_all_events": true, 616 | "spice_current_sheet_as_root": false, 617 | "spice_external_command": "spice \"%I\"", 618 | "spice_model_current_sheet_as_root": true, 619 | "spice_save_all_currents": false, 620 | "spice_save_all_dissipations": false, 621 | "spice_save_all_voltages": false, 622 | "subpart_first_id": 65, 623 | "subpart_id_separator": 0 624 | }, 625 | "sheets": [ 626 | [ 627 | "fb553d00-63d0-4a72-84d2-67ce388ec664", 628 | "Root" 629 | ] 630 | ], 631 | "text_variables": {} 632 | } 633 | -------------------------------------------------------------------------------- /adapter_pcb/flipper_as7331_adapter/flipper_as7331_adapter.kicad_sch: -------------------------------------------------------------------------------- 1 | (kicad_sch 2 | (version 20250114) 3 | (generator "eeschema") 4 | (generator_version "9.0") 5 | (uuid "fb553d00-63d0-4a72-84d2-67ce388ec664") 6 | (paper "A4") 7 | (lib_symbols 8 | (symbol "Connector_Generic:Conn_01x06" 9 | (pin_names 10 | (offset 1.016) 11 | (hide yes) 12 | ) 13 | (exclude_from_sim no) 14 | (in_bom yes) 15 | (on_board yes) 16 | (property "Reference" "J" 17 | (at 0 7.62 0) 18 | (effects 19 | (font 20 | (size 1.27 1.27) 21 | ) 22 | ) 23 | ) 24 | (property "Value" "Conn_01x06" 25 | (at 0 -10.16 0) 26 | (effects 27 | (font 28 | (size 1.27 1.27) 29 | ) 30 | ) 31 | ) 32 | (property "Footprint" "" 33 | (at 0 0 0) 34 | (effects 35 | (font 36 | (size 1.27 1.27) 37 | ) 38 | (hide yes) 39 | ) 40 | ) 41 | (property "Datasheet" "~" 42 | (at 0 0 0) 43 | (effects 44 | (font 45 | (size 1.27 1.27) 46 | ) 47 | (hide yes) 48 | ) 49 | ) 50 | (property "Description" "Generic connector, single row, 01x06, script generated (kicad-library-utils/schlib/autogen/connector/)" 51 | (at 0 0 0) 52 | (effects 53 | (font 54 | (size 1.27 1.27) 55 | ) 56 | (hide yes) 57 | ) 58 | ) 59 | (property "ki_keywords" "connector" 60 | (at 0 0 0) 61 | (effects 62 | (font 63 | (size 1.27 1.27) 64 | ) 65 | (hide yes) 66 | ) 67 | ) 68 | (property "ki_fp_filters" "Connector*:*_1x??_*" 69 | (at 0 0 0) 70 | (effects 71 | (font 72 | (size 1.27 1.27) 73 | ) 74 | (hide yes) 75 | ) 76 | ) 77 | (symbol "Conn_01x06_1_1" 78 | (rectangle 79 | (start -1.27 6.35) 80 | (end 1.27 -8.89) 81 | (stroke 82 | (width 0.254) 83 | (type default) 84 | ) 85 | (fill 86 | (type background) 87 | ) 88 | ) 89 | (rectangle 90 | (start -1.27 5.207) 91 | (end 0 4.953) 92 | (stroke 93 | (width 0.1524) 94 | (type default) 95 | ) 96 | (fill 97 | (type none) 98 | ) 99 | ) 100 | (rectangle 101 | (start -1.27 2.667) 102 | (end 0 2.413) 103 | (stroke 104 | (width 0.1524) 105 | (type default) 106 | ) 107 | (fill 108 | (type none) 109 | ) 110 | ) 111 | (rectangle 112 | (start -1.27 0.127) 113 | (end 0 -0.127) 114 | (stroke 115 | (width 0.1524) 116 | (type default) 117 | ) 118 | (fill 119 | (type none) 120 | ) 121 | ) 122 | (rectangle 123 | (start -1.27 -2.413) 124 | (end 0 -2.667) 125 | (stroke 126 | (width 0.1524) 127 | (type default) 128 | ) 129 | (fill 130 | (type none) 131 | ) 132 | ) 133 | (rectangle 134 | (start -1.27 -4.953) 135 | (end 0 -5.207) 136 | (stroke 137 | (width 0.1524) 138 | (type default) 139 | ) 140 | (fill 141 | (type none) 142 | ) 143 | ) 144 | (rectangle 145 | (start -1.27 -7.493) 146 | (end 0 -7.747) 147 | (stroke 148 | (width 0.1524) 149 | (type default) 150 | ) 151 | (fill 152 | (type none) 153 | ) 154 | ) 155 | (pin passive line 156 | (at -5.08 5.08 0) 157 | (length 3.81) 158 | (name "Pin_1" 159 | (effects 160 | (font 161 | (size 1.27 1.27) 162 | ) 163 | ) 164 | ) 165 | (number "1" 166 | (effects 167 | (font 168 | (size 1.27 1.27) 169 | ) 170 | ) 171 | ) 172 | ) 173 | (pin passive line 174 | (at -5.08 2.54 0) 175 | (length 3.81) 176 | (name "Pin_2" 177 | (effects 178 | (font 179 | (size 1.27 1.27) 180 | ) 181 | ) 182 | ) 183 | (number "2" 184 | (effects 185 | (font 186 | (size 1.27 1.27) 187 | ) 188 | ) 189 | ) 190 | ) 191 | (pin passive line 192 | (at -5.08 0 0) 193 | (length 3.81) 194 | (name "Pin_3" 195 | (effects 196 | (font 197 | (size 1.27 1.27) 198 | ) 199 | ) 200 | ) 201 | (number "3" 202 | (effects 203 | (font 204 | (size 1.27 1.27) 205 | ) 206 | ) 207 | ) 208 | ) 209 | (pin passive line 210 | (at -5.08 -2.54 0) 211 | (length 3.81) 212 | (name "Pin_4" 213 | (effects 214 | (font 215 | (size 1.27 1.27) 216 | ) 217 | ) 218 | ) 219 | (number "4" 220 | (effects 221 | (font 222 | (size 1.27 1.27) 223 | ) 224 | ) 225 | ) 226 | ) 227 | (pin passive line 228 | (at -5.08 -5.08 0) 229 | (length 3.81) 230 | (name "Pin_5" 231 | (effects 232 | (font 233 | (size 1.27 1.27) 234 | ) 235 | ) 236 | ) 237 | (number "5" 238 | (effects 239 | (font 240 | (size 1.27 1.27) 241 | ) 242 | ) 243 | ) 244 | ) 245 | (pin passive line 246 | (at -5.08 -7.62 0) 247 | (length 3.81) 248 | (name "Pin_6" 249 | (effects 250 | (font 251 | (size 1.27 1.27) 252 | ) 253 | ) 254 | ) 255 | (number "6" 256 | (effects 257 | (font 258 | (size 1.27 1.27) 259 | ) 260 | ) 261 | ) 262 | ) 263 | ) 264 | (embedded_fonts no) 265 | ) 266 | (symbol "Connector_Generic:Conn_01x10" 267 | (pin_names 268 | (offset 1.016) 269 | (hide yes) 270 | ) 271 | (exclude_from_sim no) 272 | (in_bom yes) 273 | (on_board yes) 274 | (property "Reference" "J" 275 | (at 0 12.7 0) 276 | (effects 277 | (font 278 | (size 1.27 1.27) 279 | ) 280 | ) 281 | ) 282 | (property "Value" "Conn_01x10" 283 | (at 0 -15.24 0) 284 | (effects 285 | (font 286 | (size 1.27 1.27) 287 | ) 288 | ) 289 | ) 290 | (property "Footprint" "" 291 | (at 0 0 0) 292 | (effects 293 | (font 294 | (size 1.27 1.27) 295 | ) 296 | (hide yes) 297 | ) 298 | ) 299 | (property "Datasheet" "~" 300 | (at 0 0 0) 301 | (effects 302 | (font 303 | (size 1.27 1.27) 304 | ) 305 | (hide yes) 306 | ) 307 | ) 308 | (property "Description" "Generic connector, single row, 01x10, script generated (kicad-library-utils/schlib/autogen/connector/)" 309 | (at 0 0 0) 310 | (effects 311 | (font 312 | (size 1.27 1.27) 313 | ) 314 | (hide yes) 315 | ) 316 | ) 317 | (property "ki_keywords" "connector" 318 | (at 0 0 0) 319 | (effects 320 | (font 321 | (size 1.27 1.27) 322 | ) 323 | (hide yes) 324 | ) 325 | ) 326 | (property "ki_fp_filters" "Connector*:*_1x??_*" 327 | (at 0 0 0) 328 | (effects 329 | (font 330 | (size 1.27 1.27) 331 | ) 332 | (hide yes) 333 | ) 334 | ) 335 | (symbol "Conn_01x10_1_1" 336 | (rectangle 337 | (start -1.27 11.43) 338 | (end 1.27 -13.97) 339 | (stroke 340 | (width 0.254) 341 | (type default) 342 | ) 343 | (fill 344 | (type background) 345 | ) 346 | ) 347 | (rectangle 348 | (start -1.27 10.287) 349 | (end 0 10.033) 350 | (stroke 351 | (width 0.1524) 352 | (type default) 353 | ) 354 | (fill 355 | (type none) 356 | ) 357 | ) 358 | (rectangle 359 | (start -1.27 7.747) 360 | (end 0 7.493) 361 | (stroke 362 | (width 0.1524) 363 | (type default) 364 | ) 365 | (fill 366 | (type none) 367 | ) 368 | ) 369 | (rectangle 370 | (start -1.27 5.207) 371 | (end 0 4.953) 372 | (stroke 373 | (width 0.1524) 374 | (type default) 375 | ) 376 | (fill 377 | (type none) 378 | ) 379 | ) 380 | (rectangle 381 | (start -1.27 2.667) 382 | (end 0 2.413) 383 | (stroke 384 | (width 0.1524) 385 | (type default) 386 | ) 387 | (fill 388 | (type none) 389 | ) 390 | ) 391 | (rectangle 392 | (start -1.27 0.127) 393 | (end 0 -0.127) 394 | (stroke 395 | (width 0.1524) 396 | (type default) 397 | ) 398 | (fill 399 | (type none) 400 | ) 401 | ) 402 | (rectangle 403 | (start -1.27 -2.413) 404 | (end 0 -2.667) 405 | (stroke 406 | (width 0.1524) 407 | (type default) 408 | ) 409 | (fill 410 | (type none) 411 | ) 412 | ) 413 | (rectangle 414 | (start -1.27 -4.953) 415 | (end 0 -5.207) 416 | (stroke 417 | (width 0.1524) 418 | (type default) 419 | ) 420 | (fill 421 | (type none) 422 | ) 423 | ) 424 | (rectangle 425 | (start -1.27 -7.493) 426 | (end 0 -7.747) 427 | (stroke 428 | (width 0.1524) 429 | (type default) 430 | ) 431 | (fill 432 | (type none) 433 | ) 434 | ) 435 | (rectangle 436 | (start -1.27 -10.033) 437 | (end 0 -10.287) 438 | (stroke 439 | (width 0.1524) 440 | (type default) 441 | ) 442 | (fill 443 | (type none) 444 | ) 445 | ) 446 | (rectangle 447 | (start -1.27 -12.573) 448 | (end 0 -12.827) 449 | (stroke 450 | (width 0.1524) 451 | (type default) 452 | ) 453 | (fill 454 | (type none) 455 | ) 456 | ) 457 | (pin passive line 458 | (at -5.08 10.16 0) 459 | (length 3.81) 460 | (name "Pin_1" 461 | (effects 462 | (font 463 | (size 1.27 1.27) 464 | ) 465 | ) 466 | ) 467 | (number "1" 468 | (effects 469 | (font 470 | (size 1.27 1.27) 471 | ) 472 | ) 473 | ) 474 | ) 475 | (pin passive line 476 | (at -5.08 7.62 0) 477 | (length 3.81) 478 | (name "Pin_2" 479 | (effects 480 | (font 481 | (size 1.27 1.27) 482 | ) 483 | ) 484 | ) 485 | (number "2" 486 | (effects 487 | (font 488 | (size 1.27 1.27) 489 | ) 490 | ) 491 | ) 492 | ) 493 | (pin passive line 494 | (at -5.08 5.08 0) 495 | (length 3.81) 496 | (name "Pin_3" 497 | (effects 498 | (font 499 | (size 1.27 1.27) 500 | ) 501 | ) 502 | ) 503 | (number "3" 504 | (effects 505 | (font 506 | (size 1.27 1.27) 507 | ) 508 | ) 509 | ) 510 | ) 511 | (pin passive line 512 | (at -5.08 2.54 0) 513 | (length 3.81) 514 | (name "Pin_4" 515 | (effects 516 | (font 517 | (size 1.27 1.27) 518 | ) 519 | ) 520 | ) 521 | (number "4" 522 | (effects 523 | (font 524 | (size 1.27 1.27) 525 | ) 526 | ) 527 | ) 528 | ) 529 | (pin passive line 530 | (at -5.08 0 0) 531 | (length 3.81) 532 | (name "Pin_5" 533 | (effects 534 | (font 535 | (size 1.27 1.27) 536 | ) 537 | ) 538 | ) 539 | (number "5" 540 | (effects 541 | (font 542 | (size 1.27 1.27) 543 | ) 544 | ) 545 | ) 546 | ) 547 | (pin passive line 548 | (at -5.08 -2.54 0) 549 | (length 3.81) 550 | (name "Pin_6" 551 | (effects 552 | (font 553 | (size 1.27 1.27) 554 | ) 555 | ) 556 | ) 557 | (number "6" 558 | (effects 559 | (font 560 | (size 1.27 1.27) 561 | ) 562 | ) 563 | ) 564 | ) 565 | (pin passive line 566 | (at -5.08 -5.08 0) 567 | (length 3.81) 568 | (name "Pin_7" 569 | (effects 570 | (font 571 | (size 1.27 1.27) 572 | ) 573 | ) 574 | ) 575 | (number "7" 576 | (effects 577 | (font 578 | (size 1.27 1.27) 579 | ) 580 | ) 581 | ) 582 | ) 583 | (pin passive line 584 | (at -5.08 -7.62 0) 585 | (length 3.81) 586 | (name "Pin_8" 587 | (effects 588 | (font 589 | (size 1.27 1.27) 590 | ) 591 | ) 592 | ) 593 | (number "8" 594 | (effects 595 | (font 596 | (size 1.27 1.27) 597 | ) 598 | ) 599 | ) 600 | ) 601 | (pin passive line 602 | (at -5.08 -10.16 0) 603 | (length 3.81) 604 | (name "Pin_9" 605 | (effects 606 | (font 607 | (size 1.27 1.27) 608 | ) 609 | ) 610 | ) 611 | (number "9" 612 | (effects 613 | (font 614 | (size 1.27 1.27) 615 | ) 616 | ) 617 | ) 618 | ) 619 | (pin passive line 620 | (at -5.08 -12.7 0) 621 | (length 3.81) 622 | (name "Pin_10" 623 | (effects 624 | (font 625 | (size 1.27 1.27) 626 | ) 627 | ) 628 | ) 629 | (number "10" 630 | (effects 631 | (font 632 | (size 1.27 1.27) 633 | ) 634 | ) 635 | ) 636 | ) 637 | ) 638 | (embedded_fonts no) 639 | ) 640 | ) 641 | (junction 642 | (at 100.33 74.93) 643 | (diameter 0) 644 | (color 0 0 0 0) 645 | (uuid "3036e3bd-bb69-4589-b150-b7aabc6769c8") 646 | ) 647 | (junction 648 | (at 102.87 74.93) 649 | (diameter 0) 650 | (color 0 0 0 0) 651 | (uuid "7aad4664-608e-436c-85d8-51a66f0b88e2") 652 | ) 653 | (junction 654 | (at 90.17 74.93) 655 | (diameter 0) 656 | (color 0 0 0 0) 657 | (uuid "928dde94-38f7-4c05-bf6e-ee5f853f74d0") 658 | ) 659 | (junction 660 | (at 85.09 74.93) 661 | (diameter 0) 662 | (color 0 0 0 0) 663 | (uuid "d88c90b2-03b5-41b8-a1ad-8f7dfb2c5a93") 664 | ) 665 | (wire 666 | (pts 667 | (xy 105.41 74.93) (xy 105.41 99.06) 668 | ) 669 | (stroke 670 | (width 0) 671 | (type default) 672 | ) 673 | (uuid "08710686-de34-406c-a7f9-fa4feeb63afc") 674 | ) 675 | (wire 676 | (pts 677 | (xy 102.87 74.93) (xy 102.87 99.06) 678 | ) 679 | (stroke 680 | (width 0) 681 | (type default) 682 | ) 683 | (uuid "135925b3-67a7-49f2-a19b-2cd18df980ed") 684 | ) 685 | (wire 686 | (pts 687 | (xy 95.25 49.53) (xy 95.25 64.77) 688 | ) 689 | (stroke 690 | (width 0) 691 | (type default) 692 | ) 693 | (uuid "14a9b9bf-aada-490b-bd66-1141f527dfe9") 694 | ) 695 | (wire 696 | (pts 697 | (xy 90.17 64.77) (xy 90.17 74.93) 698 | ) 699 | (stroke 700 | (width 0) 701 | (type default) 702 | ) 703 | (uuid "17bea2b3-2feb-41af-8337-c9740e9e02b8") 704 | ) 705 | (wire 706 | (pts 707 | (xy 90.17 74.93) (xy 90.17 99.06) 708 | ) 709 | (stroke 710 | (width 0) 711 | (type default) 712 | ) 713 | (uuid "239487ec-e108-4b67-a406-7a27a7a21899") 714 | ) 715 | (wire 716 | (pts 717 | (xy 97.79 49.53) (xy 97.79 62.23) 718 | ) 719 | (stroke 720 | (width 0) 721 | (type default) 722 | ) 723 | (uuid "295be433-3253-4d1b-9c1f-605c8eb9b41f") 724 | ) 725 | (wire 726 | (pts 727 | (xy 102.87 49.53) (xy 102.87 74.93) 728 | ) 729 | (stroke 730 | (width 0) 731 | (type default) 732 | ) 733 | (uuid "43ffe40d-72f4-4d17-860b-267343189ee8") 734 | ) 735 | (wire 736 | (pts 737 | (xy 85.09 62.23) (xy 85.09 74.93) 738 | ) 739 | (stroke 740 | (width 0) 741 | (type default) 742 | ) 743 | (uuid "457e1919-847f-43c1-883b-827d1cd58de2") 744 | ) 745 | (wire 746 | (pts 747 | (xy 92.71 74.93) (xy 92.71 99.06) 748 | ) 749 | (stroke 750 | (width 0) 751 | (type default) 752 | ) 753 | (uuid "4f56b63c-f686-42ed-9e49-9a7849e85146") 754 | ) 755 | (wire 756 | (pts 757 | (xy 95.25 74.93) (xy 95.25 99.06) 758 | ) 759 | (stroke 760 | (width 0) 761 | (type default) 762 | ) 763 | (uuid "79773e13-e9c0-4857-974e-9c739a59a193") 764 | ) 765 | (wire 766 | (pts 767 | (xy 107.95 74.93) (xy 107.95 99.06) 768 | ) 769 | (stroke 770 | (width 0) 771 | (type default) 772 | ) 773 | (uuid "8790d430-dcde-49d2-a191-624715eb750b") 774 | ) 775 | (wire 776 | (pts 777 | (xy 100.33 74.93) (xy 100.33 99.06) 778 | ) 779 | (stroke 780 | (width 0) 781 | (type default) 782 | ) 783 | (uuid "887f238f-6b43-4428-9869-e065f2e76dd6") 784 | ) 785 | (wire 786 | (pts 787 | (xy 95.25 64.77) (xy 90.17 64.77) 788 | ) 789 | (stroke 790 | (width 0) 791 | (type default) 792 | ) 793 | (uuid "a65d7427-74ee-41e7-be3b-ebbf52528469") 794 | ) 795 | (wire 796 | (pts 797 | (xy 87.63 74.93) (xy 87.63 99.06) 798 | ) 799 | (stroke 800 | (width 0) 801 | (type default) 802 | ) 803 | (uuid "b9a2b3c2-73da-4b85-984c-721ba4031ff7") 804 | ) 805 | (wire 806 | (pts 807 | (xy 97.79 62.23) (xy 85.09 62.23) 808 | ) 809 | (stroke 810 | (width 0) 811 | (type default) 812 | ) 813 | (uuid "c2112f1b-6566-4038-b345-7d4e9d3d6de6") 814 | ) 815 | (wire 816 | (pts 817 | (xy 85.09 74.93) (xy 85.09 99.06) 818 | ) 819 | (stroke 820 | (width 0) 821 | (type default) 822 | ) 823 | (uuid "dbb1437f-bbc8-4216-9213-4350bca4d59e") 824 | ) 825 | (wire 826 | (pts 827 | (xy 97.79 74.93) (xy 97.79 99.06) 828 | ) 829 | (stroke 830 | (width 0) 831 | (type default) 832 | ) 833 | (uuid "e0450e01-a9c3-4afa-b14f-c5e423bed220") 834 | ) 835 | (wire 836 | (pts 837 | (xy 100.33 49.53) (xy 100.33 74.93) 838 | ) 839 | (stroke 840 | (width 0) 841 | (type default) 842 | ) 843 | (uuid "ee12a64e-425d-4522-acf9-841fe61359e2") 844 | ) 845 | (symbol 846 | (lib_id "Connector_Generic:Conn_01x06") 847 | (at 95.25 44.45 90) 848 | (unit 1) 849 | (exclude_from_sim no) 850 | (in_bom yes) 851 | (on_board yes) 852 | (dnp no) 853 | (fields_autoplaced yes) 854 | (uuid "1488a970-fb2d-4d6d-bcba-cb5f7edb5751") 855 | (property "Reference" "AS1" 856 | (at 96.52 38.1 90) 857 | (effects 858 | (font 859 | (size 1.27 1.27) 860 | ) 861 | (hide yes) 862 | ) 863 | ) 864 | (property "Value" "AS7331" 865 | (at 96.52 40.64 90) 866 | (effects 867 | (font 868 | (size 1.27 1.27) 869 | ) 870 | ) 871 | ) 872 | (property "Footprint" "Connector_PinSocket_2.54mm:PinSocket_1x06_P2.54mm_Vertical" 873 | (at 95.25 44.45 0) 874 | (effects 875 | (font 876 | (size 1.27 1.27) 877 | ) 878 | (hide yes) 879 | ) 880 | ) 881 | (property "Datasheet" "~" 882 | (at 95.25 44.45 0) 883 | (effects 884 | (font 885 | (size 1.27 1.27) 886 | ) 887 | (hide yes) 888 | ) 889 | ) 890 | (property "Description" "Generic connector, single row, 01x06, script generated (kicad-library-utils/schlib/autogen/connector/)" 891 | (at 95.25 44.45 0) 892 | (effects 893 | (font 894 | (size 1.27 1.27) 895 | ) 896 | (hide yes) 897 | ) 898 | ) 899 | (pin "1" 900 | (uuid "91a07d06-bc98-4496-a60b-5acf926425ca") 901 | ) 902 | (pin "4" 903 | (uuid "30bc6643-bac2-4b27-94fd-aba71355d883") 904 | ) 905 | (pin "2" 906 | (uuid "0a43c3fb-d9fb-4174-b3d4-35a175b18bbb") 907 | ) 908 | (pin "3" 909 | (uuid "d6527eb9-a98b-421f-ba4a-3d4fe077b7c8") 910 | ) 911 | (pin "6" 912 | (uuid "372f5b66-43f3-44d2-a835-a8bbea449213") 913 | ) 914 | (pin "5" 915 | (uuid "296ff39f-8d17-4519-904b-204c652954d1") 916 | ) 917 | (instances 918 | (project "" 919 | (path "/fb553d00-63d0-4a72-84d2-67ce388ec664" 920 | (reference "AS1") 921 | (unit 1) 922 | ) 923 | ) 924 | ) 925 | ) 926 | (symbol 927 | (lib_id "Connector_Generic:Conn_01x10") 928 | (at 95.25 104.14 90) 929 | (mirror x) 930 | (unit 1) 931 | (exclude_from_sim no) 932 | (in_bom yes) 933 | (on_board yes) 934 | (dnp no) 935 | (uuid "344973ec-ce9d-430d-9426-0923f56deeae") 936 | (property "Reference" "Flipper1" 937 | (at 96.52 107.95 90) 938 | (effects 939 | (font 940 | (size 1.27 1.27) 941 | ) 942 | (hide yes) 943 | ) 944 | ) 945 | (property "Value" "Flipper" 946 | (at 96.52 110.49 90) 947 | (effects 948 | (font 949 | (size 1.27 1.27) 950 | ) 951 | ) 952 | ) 953 | (property "Footprint" "Connector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_Horizontal" 954 | (at 95.25 104.14 0) 955 | (effects 956 | (font 957 | (size 1.27 1.27) 958 | ) 959 | (hide yes) 960 | ) 961 | ) 962 | (property "Datasheet" "~" 963 | (at 95.25 104.14 0) 964 | (effects 965 | (font 966 | (size 1.27 1.27) 967 | ) 968 | (hide yes) 969 | ) 970 | ) 971 | (property "Description" "Generic connector, single row, 01x10, script generated (kicad-library-utils/schlib/autogen/connector/)" 972 | (at 95.25 104.14 0) 973 | (effects 974 | (font 975 | (size 1.27 1.27) 976 | ) 977 | (hide yes) 978 | ) 979 | ) 980 | (pin "1" 981 | (uuid "566eb4ef-d103-4ce7-b1ca-e65b804c98d5") 982 | ) 983 | (pin "9" 984 | (uuid "fa67e429-80af-4e9a-ab8c-d7e514d87aa7") 985 | ) 986 | (pin "8" 987 | (uuid "b2e5deaa-b042-41f4-a1d2-9b89d74654bc") 988 | ) 989 | (pin "7" 990 | (uuid "8db6c1f2-8987-40e8-a652-0109e4f63b15") 991 | ) 992 | (pin "6" 993 | (uuid "eb17cc67-3b1e-4bcf-8106-7a4653a0b0b2") 994 | ) 995 | (pin "5" 996 | (uuid "3111296c-234b-4281-8ea3-c8bd243bcffa") 997 | ) 998 | (pin "4" 999 | (uuid "e2a5b880-a23b-4b7b-a499-8ff4850d4148") 1000 | ) 1001 | (pin "3" 1002 | (uuid "31c23975-2a63-4479-b6e0-93ec59b10b88") 1003 | ) 1004 | (pin "10" 1005 | (uuid "22398e23-7c0a-4212-8574-cbbee102fd81") 1006 | ) 1007 | (pin "2" 1008 | (uuid "6cbaf3e6-083a-4996-a63f-b74e2393913c") 1009 | ) 1010 | (instances 1011 | (project "" 1012 | (path "/fb553d00-63d0-4a72-84d2-67ce388ec664" 1013 | (reference "Flipper1") 1014 | (unit 1) 1015 | ) 1016 | ) 1017 | ) 1018 | ) 1019 | (symbol 1020 | (lib_id "Connector_Generic:Conn_01x10") 1021 | (at 95.25 80.01 90) 1022 | (mirror x) 1023 | (unit 1) 1024 | (exclude_from_sim no) 1025 | (in_bom yes) 1026 | (on_board yes) 1027 | (dnp no) 1028 | (uuid "80c79d57-c70c-4a81-8380-e28ab62acb8a") 1029 | (property "Reference" "J1" 1030 | (at 96.52 83.82 90) 1031 | (effects 1032 | (font 1033 | (size 1.27 1.27) 1034 | ) 1035 | (hide yes) 1036 | ) 1037 | ) 1038 | (property "Value" "Conn_01x10" 1039 | (at 96.52 86.36 90) 1040 | (effects 1041 | (font 1042 | (size 1.27 1.27) 1043 | ) 1044 | (hide yes) 1045 | ) 1046 | ) 1047 | (property "Footprint" "Connector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_Vertical" 1048 | (at 95.25 80.01 0) 1049 | (effects 1050 | (font 1051 | (size 1.27 1.27) 1052 | ) 1053 | (hide yes) 1054 | ) 1055 | ) 1056 | (property "Datasheet" "~" 1057 | (at 95.25 80.01 0) 1058 | (effects 1059 | (font 1060 | (size 1.27 1.27) 1061 | ) 1062 | (hide yes) 1063 | ) 1064 | ) 1065 | (property "Description" "Generic connector, single row, 01x10, script generated (kicad-library-utils/schlib/autogen/connector/)" 1066 | (at 95.25 80.01 0) 1067 | (effects 1068 | (font 1069 | (size 1.27 1.27) 1070 | ) 1071 | (hide yes) 1072 | ) 1073 | ) 1074 | (pin "3" 1075 | (uuid "1154e5d8-878e-42e3-a435-f839884b2dcd") 1076 | ) 1077 | (pin "4" 1078 | (uuid "0704c16a-b329-40c6-9494-12ea80519261") 1079 | ) 1080 | (pin "5" 1081 | (uuid "d0b83c14-4d6e-42ee-9abe-dde8146f7204") 1082 | ) 1083 | (pin "6" 1084 | (uuid "61ac6e6c-00df-41b7-90f1-f6f101402aea") 1085 | ) 1086 | (pin "1" 1087 | (uuid "4e090891-775c-4b87-a39b-8997f4a561d4") 1088 | ) 1089 | (pin "2" 1090 | (uuid "4db8ca91-5f7a-47cd-9555-f403a76e1cda") 1091 | ) 1092 | (pin "7" 1093 | (uuid "2b998f54-551d-4a94-9380-b6e8f81ee174") 1094 | ) 1095 | (pin "8" 1096 | (uuid "16b767ea-1983-4004-aeab-828ce9f43b48") 1097 | ) 1098 | (pin "9" 1099 | (uuid "316cd05d-7e31-44f1-ab56-5ac55e4d7dd3") 1100 | ) 1101 | (pin "10" 1102 | (uuid "4e007b84-9cd4-4e49-a4a8-b947ba87f355") 1103 | ) 1104 | (instances 1105 | (project "" 1106 | (path "/fb553d00-63d0-4a72-84d2-67ce388ec664" 1107 | (reference "J1") 1108 | (unit 1) 1109 | ) 1110 | ) 1111 | ) 1112 | ) 1113 | (sheet_instances 1114 | (path "/" 1115 | (page "1") 1116 | ) 1117 | ) 1118 | (embedded_fonts no) 1119 | ) 1120 | -------------------------------------------------------------------------------- /AS7331.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file AS7331.hpp 3 | * @brief UV Spectral Sensor - AS7331 Driver 4 | * 5 | * This driver provides a communication interface for the AS7331 UV spectral sensor. 6 | * It includes configurations for I2C addressing, device modes, gain, clock frequency, 7 | * and measurement settings. 8 | * 9 | * Inspired by: https://github.com/sparkfun/SparkFun_AS7331_Arduino_Library/blob/main/src/sfeAS7331.h 10 | * Datasheet: https://look.ams-osram.com/m/1856fd2c69c35605/original/AS7331-Spectral-UVA-B-C-Sensor.pdf 11 | */ 12 | 13 | #pragma once 14 | 15 | #include 16 | #include 17 | 18 | #include 19 | 20 | /** 21 | * @defgroup AS7331_I2C_Addressing I2C Addressing 22 | * @brief I2C address definitions for AS7331 23 | * 24 | * The 7-bit I2C address is defined as [1, 1, 1, 0, 1, A1, A0], where A1 and A0 are configurable address pins. 25 | * @{ 26 | */ 27 | 28 | /** Default I2C address when A1 = 0, A0 = 0 */ 29 | const uint8_t DefaultI2CAddr = 0x74; 30 | 31 | /** Secondary I2C address when A1 = 0, A0 = 1 */ 32 | const uint8_t SecondaryI2CAddr = 0x75; 33 | 34 | /** Tertiary I2C address when A1 = 1, A0 = 0 */ 35 | const uint8_t TertiaryI2CAddr = 0x76; 36 | 37 | /** Quaternary I2C address when A1 = 1, A0 = 1 */ 38 | const uint8_t QuaternaryI2CAddr = 0x77; 39 | 40 | /** Expected API Generation Register content (Device ID + Mutation number) */ 41 | const uint8_t ExpectedAGENContent = 0x21; 42 | 43 | /** @} */ // End of AS7331_I2C_Addressing group 44 | 45 | /** 46 | * @defgroup AS7331_Enums Enumerations 47 | * @brief Enumerations for AS7331 settings 48 | * @{ 49 | */ 50 | 51 | /** 52 | * @brief Device Operating Modes 53 | */ 54 | enum as7331_device_mode_t : uint8_t { 55 | DEVICE_MODE_CONFIG = 0x2, /**< Configuration mode */ 56 | DEVICE_MODE_MEASURE = 0x3 /**< Measurement mode */ 57 | }; 58 | 59 | /** 60 | * @brief Sensor Gain Settings (CREG1:GAIN) 61 | * 62 | * Default: GAIN_2 (1010) 63 | * 64 | * Gain value is calculated as: 65 | * **gain = 2(11 - gain_code)** 66 | */ 67 | enum as7331_gain_t : uint8_t { 68 | GAIN_2048 = 0x0000, 69 | GAIN_1024, 70 | GAIN_512, 71 | GAIN_256, 72 | GAIN_128, 73 | GAIN_64, 74 | GAIN_32, 75 | GAIN_16, 76 | GAIN_8, 77 | GAIN_4, 78 | GAIN_2, 79 | GAIN_1 80 | }; 81 | 82 | /** 83 | * @brief Integration Time Settings (CREG1:TIME) 84 | * 85 | * Default: TIME_64MS (0110) 86 | * 87 | * The conversion time in the enumerators (in milliseconds) is valid for a clock frequency of 1.024 MHz. 88 | * For higher clock frequencies, the conversion time is divided by 2 each time the clock frequency is doubled. 89 | * 90 | * **Conversion Time [ms] = 2integration_time_code** 91 | */ 92 | enum as7331_integration_time_t : uint8_t { 93 | TIME_1MS = 0x0, 94 | TIME_2MS, 95 | TIME_4MS, 96 | TIME_8MS, 97 | TIME_16MS, 98 | TIME_32MS, 99 | TIME_64MS, 100 | TIME_128MS, 101 | TIME_256MS, 102 | TIME_512MS, 103 | TIME_1024MS, 104 | TIME_2048MS, 105 | TIME_4096MS, 106 | TIME_8192MS, 107 | TIME_16384MS 108 | }; 109 | 110 | /** 111 | * @brief Divider Settings (CREG2:DIV) 112 | * 113 | * Default: DIV_2 (000) 114 | * 115 | * Divider value is calculated as: 116 | * **divider = 2(1 + divider_code)** 117 | */ 118 | enum as7331_divider_t : uint8_t { 119 | DIV_2 = 0x0, 120 | DIV_4, 121 | DIV_8, 122 | DIV_16, 123 | DIV_32, 124 | DIV_64, 125 | DIV_128, 126 | DIV_256 127 | }; 128 | 129 | /** 130 | * @brief Internal Clock Frequency Settings fCLK (CREG3:CCLK) 131 | * 132 | * Default: CCLK_1_024_MHZ 133 | * 134 | * Clock frequency is calculated as: 135 | * **clock_frequency = 1.024 × 2clock_frequency_code** (in MHz) 136 | */ 137 | enum as7331_clock_frequency_t : uint8_t { 138 | CCLK_1_024_MHZ = 0x00, /**< 1.024 MHz: 1 - 16384 ms (Conversion Time) */ 139 | CCLK_2_048_MHZ, 140 | CCLK_4_096_MHZ, 141 | CCLK_8_192_MHZ /**< 8.192 MHz: 0.125 - 2048 ms (Conversion Time) */ 142 | }; 143 | 144 | /** 145 | * @brief Measurement Modes (CREG3:MMODE) 146 | * 147 | * Default: MEASUREMENT_MODE_COMMAND (01) 148 | */ 149 | enum as7331_measurement_mode_t : uint8_t { 150 | MEASUREMENT_MODE_CONTINUOUS = 0x0, /**< Continuous Measurement Mode (CONT) */ 151 | MEASUREMENT_MODE_COMMAND, /**< Command Measurement Mode (CMD) */ 152 | MEASUREMENT_MODE_SYNC_START, /**< Synchronous Measurement Mode (SYNS) - externally synchronized start of measurement */ 153 | MEASUREMENT_MODE_SYNC_START_END /**< Synchronous Measurement Start and End Mode (SYND) - start and end of measurement are externally synchronized */ 154 | }; 155 | 156 | /** @brief UV Type Selection */ 157 | enum as7331_uv_type_t : uint8_t { 158 | UV_A, 159 | UV_B, 160 | UV_C 161 | }; 162 | 163 | /** @} */ // End of AS7331_Enums group 164 | 165 | /** 166 | * @defgroup AS7331_Config_Registers Configuration Register Definitions 167 | * @brief Definitions for configuration registers 168 | * 169 | * Registers are 8 bits long and can only be accessed in the configuration mode. 170 | * @{ 171 | */ 172 | 173 | /** @brief OSR configuration register address */ 174 | const uint8_t RegCfgOsr = 0x00; 175 | 176 | /** 177 | * @brief Operational State Register (OSR) Register Layout 178 | */ 179 | typedef union { 180 | struct { 181 | as7331_device_mode_t 182 | operating_state : 3; /**< DOS (010) - Device Operating State (OSR[2:0]) */ 183 | uint8_t software_reset : 1; /**< SW_RES (0) - Software Reset (OSR[3]) */ 184 | uint8_t reserved : 2; /**< Reserved, do not write (OSR[5:4]) */ 185 | uint8_t power_down : 1; /**< PD (1) - Power Down (OSR[6]) */ 186 | uint8_t start_state : 1; /**< SS (0) - Start State (OSR[7]) */ 187 | }; 188 | uint8_t byte; 189 | } as7331_osr_reg_t; 190 | 191 | /** @brief AGEN register address */ 192 | const uint8_t RegCfgAgen = 0x02; 193 | 194 | /** 195 | * @brief AGEN Register Layout 196 | */ 197 | typedef union { 198 | struct { 199 | uint8_t 200 | mutation : 4; /**< MUT (0001) - Mutation number of control register bank. Incremented when control registers change (AGEN[3:0]) */ 201 | uint8_t device_id : 4; /**< DEVID (0010) - Device ID number (AGEN[7:4]) */ 202 | }; 203 | uint8_t byte; 204 | } as7331_agen_reg_t; 205 | 206 | /** @brief CREG1 -- Configuration Register 1 address */ 207 | const uint8_t RegCfgCreg1 = 0x06; 208 | 209 | /** 210 | * @brief CREG1 Register Layout 211 | */ 212 | typedef union { 213 | struct { 214 | as7331_integration_time_t 215 | integration_time : 4; /**< TIME (0110) - Integration time (CREG1[3:0]) */ 216 | as7331_gain_t gain : 4; /**< GAIN (1010) - Sensor gain (CREG1[7:4]) */ 217 | }; 218 | uint8_t byte; 219 | } as7331_creg1_reg_t; 220 | 221 | /** @brief CREG2 -- Configuration Register 2 address */ 222 | const uint8_t RegCfgCreg2 = 0x07; 223 | 224 | /** 225 | * @brief CREG2 Register Layout 226 | */ 227 | typedef union { 228 | struct { 229 | as7331_divider_t divider : 3; /**< DIV (000) - Divider value (CREG2[2:0]) */ 230 | uint8_t enable_divider : 1; /**< EN_DIV (0) - Divider enable (CREG2[3]) */ 231 | uint8_t reserved : 2; /**< Reserved, do not write (CREG2[5:4]) */ 232 | uint8_t 233 | enable_temp : 1; /**< EN_TM (1) - Temperature measurement enable in SYND mode (CREG2[6]) */ 234 | uint8_t reserved1 : 1; /**< Reserved, do not write (CREG2[7]) */ 235 | }; 236 | uint8_t byte; 237 | } as7331_creg2_reg_t; 238 | 239 | /** @brief CREG3 -- Configuration Register 3 address */ 240 | const uint8_t RegCfgCreg3 = 0x08; 241 | 242 | /** 243 | * @brief CREG3 Register Layout 244 | */ 245 | typedef union { 246 | struct { 247 | as7331_clock_frequency_t 248 | clock_frequency : 2; /**< CCLK (00) - Internal clock frequency (CREG3[1:0]) */ 249 | uint8_t reserved : 1; /**< Reserved, do not write (CREG3[2]) */ 250 | uint8_t ready_mode : 1; /**< RDYOD (0) - Ready pin mode (CREG3[3]) */ 251 | uint8_t standby : 1; /**< SB (0) - Standby mode (CREG3[4]) */ 252 | uint8_t reserved1 : 1; /**< Reserved, do not write (CREG3[5]) */ 253 | as7331_measurement_mode_t 254 | measurement_mode : 2; /**< MMODE (01) - Measurement mode selection (CREG3[7:6]) */ 255 | }; 256 | uint8_t byte; 257 | } as7331_creg3_reg_t; 258 | 259 | /** @brief BREAK register address */ 260 | const uint8_t RegCfgBreak = 261 | 0x09; /**< BREAK (0x19) - Break time TBREAK between two measurements (except CMD mode) */ 262 | 263 | /** @brief EDGES register address */ 264 | const uint8_t RegCfgEdges = 0x0A; /**< EDGES (0x1) - Number of SYN falling edges */ 265 | 266 | /** @brief OPTREG - Option Register address */ 267 | const uint8_t RegCfgOptReg = 0x0B; 268 | 269 | /** 270 | * @brief OPTREG Register Layout 271 | */ 272 | typedef union { 273 | struct { 274 | uint8_t init_idx : 1; /**< INIT_IDX (1) - I2C repeat start mode flag (OPTREG[0]) */ 275 | uint8_t reserved : 7; /**< Reserved, do not write (OPTREG[7:1]) */ 276 | }; 277 | uint8_t byte; 278 | } as7331_optreg_reg_t; 279 | 280 | /** @} */ // End of AS7331_Config_Registers group 281 | 282 | /** 283 | * @defgroup AS7331_Measurement_Registers Measurement Register Definitions 284 | * @brief Definitions for measurement registers 285 | * 286 | * Registers are 16 bits long and read-only, except for the lower byte of the OSR/STATUS register. 287 | * @{ 288 | */ 289 | 290 | /** @brief OSR/Status register address */ 291 | const uint8_t RegMeasOsrStatus = 0x00; 292 | 293 | /** 294 | * @brief OSR/STATUS Register Layout 295 | */ 296 | typedef union { 297 | struct { 298 | as7331_osr_reg_t osr; /**< OSR settings (lower byte) (OSRSTAT[7:0]) */ 299 | uint8_t power_state : 1; /**< POWERSTATE - Power Down state (OSRSTAT[8]) */ 300 | uint8_t standby_state : 1; /**< STANDBYSTATE - Standby mode state (OSRSTAT[9]) */ 301 | uint8_t not_ready : 1; /**< NOTREADY - Inverted ready pin state (OSRSTAT[10]) */ 302 | uint8_t new_data : 1; /**< NDATA - New data available (OSRSTAT[11]) */ 303 | uint8_t lost_data : 1; /**< LDATA - Data overwritten before retrieval (OSRSTAT[12]) */ 304 | uint8_t adc_overflow : 1; /**< ADCOF - Overflow of ADC channel (OSRSTAT[13]) */ 305 | uint8_t result_overflow : 1; /**< MRESOF - Overflow of MRES1...MRES3 (OSRSTAT[14]) */ 306 | uint8_t 307 | out_conv_overflow : 1; /**< OUTCONVOF - Overflow of internal 24-bit OUTCONV (OSRSTAT[15]) */ 308 | }; 309 | uint16_t word; 310 | } as7331_osr_status_reg_t; 311 | 312 | /** @brief TEMP register address (12-bit temperature, MS 4 bits are 0) */ 313 | const uint8_t RegMeasTemp = 0x01; 314 | 315 | /** @brief MRES1 register address - Measurement Result A */ 316 | const uint8_t RegMeasResultA = 0x02; 317 | 318 | /** @brief MRES2 register address - Measurement Result B */ 319 | const uint8_t RegMeasResultB = 0x03; 320 | 321 | /** @brief MRES3 register address - Measurement Result C */ 322 | const uint8_t RegMeasResultC = 0x04; 323 | 324 | /** @brief OUTCONVL register address - First 16 bits of 24-bit OUTCONV */ 325 | const uint8_t RegMeasOutConvL = 0x05; 326 | 327 | /** @brief OUTCONVH register address - Upper 8 bits of OUTCONV, MSB is 0 */ 328 | const uint8_t RegMeasOutConvH = 0x06; 329 | 330 | /** @} */ // End of AS7331_Measurement_Registers group 331 | 332 | /** 333 | * @brief AS7331 UV Spectral Sensor Driver 334 | * 335 | * This class provides an interface for the AS7331 UV spectral sensor, 336 | * allowing configuration and measurement. 337 | */ 338 | class AS7331 { 339 | public: 340 | /** @brief Struct to hold raw measurement results */ 341 | struct RawResults { 342 | uint16_t uv_a; /**< Raw measurement for UV-A channel */ 343 | uint16_t uv_b; /**< Raw measurement for UV-B channel */ 344 | uint16_t uv_c; /**< Raw measurement for UV-C channel */ 345 | }; 346 | 347 | /** @brief Struct to hold processed measurement results */ 348 | struct Results { 349 | double uv_a; /**< Irradiance for UV-A in µW/cm² */ 350 | double uv_b; /**< Irradiance for UV-B in µW/cm² */ 351 | double uv_c; /**< Irradiance for UV-C in µW/cm² */ 352 | }; 353 | 354 | /** 355 | * @brief Constructs an AS7331 sensor object 356 | * 357 | * @param address I2C address of the sensor (default is DefaultI2CAddr) 358 | */ 359 | AS7331(uint8_t address = DefaultI2CAddr); 360 | 361 | /** 362 | * @brief Initialize the sensor 363 | * 364 | * @param address I2C address in 7-bit format. If 0x0, scan the I2C bus to find the device. 365 | * @return true if initialization is successful, false otherwise 366 | */ 367 | bool init(const uint8_t& address = 0); 368 | 369 | // Configuration methods 370 | 371 | /** 372 | * @brief Set sensor gain (CREG1:GAIN) 373 | * 374 | * @param gain Gain setting from as7331_gain_t 375 | * @return true if successful, false otherwise 376 | */ 377 | bool setGain(const as7331_gain_t& gain); 378 | 379 | /** 380 | * @brief Set integration time (CREG1:TIME) 381 | * 382 | * @param time Integration time setting from as7331_integration_time_t 383 | * @return true if successful, false otherwise 384 | */ 385 | bool setIntegrationTime(const as7331_integration_time_t& time); 386 | 387 | /** 388 | * @brief Set output divider (CREG2:DIV) 389 | * 390 | * @param divider Divider setting from as7331_divider_t 391 | * @param enable Enable or disable the divider 392 | * @return true if successful, false otherwise 393 | */ 394 | bool setDivider(const as7331_divider_t& divider, const bool enable = true); 395 | 396 | /** 397 | * @brief Set clock frequency (CREG3:CCLK) 398 | * 399 | * @param freq Clock frequency setting from as7331_clock_frequency_t 400 | * @return true if successful, false otherwise 401 | */ 402 | bool setClockFrequency(const as7331_clock_frequency_t& freq); 403 | 404 | /** 405 | * @brief Set measurement mode (CREG3:MMODE) 406 | * 407 | * @param mode Measurement mode setting from as7331_measurement_mode_t 408 | * @return true if successful, false otherwise 409 | */ 410 | bool setMeasurementMode(const as7331_measurement_mode_t& mode); 411 | 412 | // Power management 413 | 414 | /** 415 | * @brief Enable or disable power-down state (OSR:PD) 416 | * 417 | * @param power_down true to enable power-down, false to disable 418 | * @return true if successful, false otherwise 419 | */ 420 | bool setPowerDown(const bool& power_down); 421 | 422 | /** 423 | * @brief Enable or disable standby mode (CREG3:SB) 424 | * 425 | * @param standby true to enable standby, false to disable 426 | * @return true if successful, false otherwise 427 | */ 428 | bool setStandby(const bool& standby); 429 | 430 | // Measurement methods 431 | 432 | /** 433 | * @brief Start the measurement (OSR:SS = 1) 434 | * 435 | * @return true if successful, false otherwise 436 | */ 437 | bool startMeasurement(); 438 | 439 | /** 440 | * @brief Stop the measurement (OSR:SS = 0) 441 | * 442 | * @return true if successful, false otherwise 443 | */ 444 | bool stopMeasurement(); 445 | 446 | /** 447 | * @brief Wait for the measurement to complete based on conversion time 448 | */ 449 | void waitForMeasurement(); 450 | 451 | /** 452 | * @brief Get raw measurement results 453 | * 454 | * Before reading results, wait an appropriate amount of time for the measurement to complete (TCONV). 455 | * 456 | * @param rawResults Struct to hold raw measurement results 457 | * @return true if successful, false otherwise 458 | */ 459 | bool getRawResults(RawResults& rawResults); 460 | 461 | /** 462 | * @brief Get processed measurement results 463 | * 464 | * Before reading results, wait an appropriate amount of time for the measurement to complete (TCONV). 465 | * 466 | * @param results Struct to hold processed measurement results 467 | * @param rawResults Struct to hold raw measurement results 468 | * @return true if successful, false otherwise 469 | */ 470 | bool getResults(Results& results, RawResults& rawResults); 471 | 472 | /** 473 | * @brief Get processed measurement results 474 | * 475 | * Before reading results, wait an appropriate amount of time for the measurement to complete (TCONV). 476 | * 477 | * @param results Struct to hold processed measurement results 478 | * @return true if successful, false otherwise 479 | */ 480 | bool getResults(Results& results); 481 | 482 | /** 483 | * @brief Get temperature measurement 484 | * 485 | * Before reading results, wait an appropriate amount of time for the measurement to complete (TCONV). 486 | * 487 | * @param temperature Temperature in degrees Celsius 488 | * @return true if successful, false otherwise 489 | */ 490 | bool getTemperature(double& temperature); 491 | 492 | // Utility methods 493 | 494 | /** 495 | * @brief Check if the device is ready 496 | * 497 | * @param i2c_address_7bit I2C address in 7-bit format (default uses internal address) 498 | * @return true if device is ready, false otherwise 499 | */ 500 | bool deviceReady(const uint8_t& i2c_address_7bit = 0); 501 | 502 | /** 503 | * @brief Perform a software reset 504 | * 505 | * This immediately stops any running measurements and resets the device to its configuration state, 506 | * with all registers reverting to their initial values. 507 | * 508 | * @return true if the reset was successful, false otherwise 509 | */ 510 | bool reset(); 511 | 512 | // Getter methods 513 | 514 | /** 515 | * @brief Get device ID (including mutation number) 516 | * 517 | * @param deviceID Struct to hold device ID 518 | * @return true if successful, false otherwise 519 | */ 520 | bool getDeviceID(as7331_agen_reg_t& deviceID); 521 | 522 | /** 523 | * @brief Get status register in measurement mode 524 | * 525 | * @param status Struct to hold status register data 526 | * @return true if successful, false otherwise 527 | */ 528 | bool getStatus(as7331_osr_status_reg_t& status); 529 | 530 | // Local config getter methods 531 | 532 | /** 533 | * @brief Get current gain setting 534 | * 535 | * @return Current gain setting as as7331_gain_t 536 | */ 537 | as7331_gain_t getGain() const; 538 | 539 | /** 540 | * @brief Get actual gain value 541 | * 542 | * @return Gain value as integer 543 | */ 544 | int16_t getGainValue() const; 545 | 546 | /** 547 | * @brief Get current integration time setting 548 | * 549 | * @return Current integration time as as7331_integration_time_t 550 | */ 551 | as7331_integration_time_t getIntegrationTime() const; 552 | 553 | /** 554 | * @brief Get current conversion time in seconds 555 | * 556 | * @return Conversion time in seconds 557 | */ 558 | double getConversionTime() const; 559 | 560 | /** 561 | * @brief Get current divider setting 562 | * 563 | * @return Current divider setting as as7331_divider_t 564 | */ 565 | as7331_divider_t getDivider() const; 566 | 567 | /** 568 | * @brief Check if divider is enabled 569 | * 570 | * @return true if divider is enabled, false otherwise 571 | */ 572 | bool isDividerEnabled() const; 573 | 574 | /** 575 | * @brief Get actual divider value 576 | * 577 | * @return Divider value as integer 578 | */ 579 | uint8_t getDividerValue() const; 580 | 581 | /** 582 | * @brief Get current clock frequency setting 583 | * 584 | * @return Current clock frequency as as7331_clock_frequency_t 585 | */ 586 | as7331_clock_frequency_t getClockFrequency() const; 587 | 588 | /** 589 | * @brief Get actual clock frequency value 590 | * 591 | * @return Clock frequency in MHz 592 | */ 593 | double getClockFrequencyValue() const; 594 | 595 | /** 596 | * @brief Get current measurement mode 597 | * 598 | * @return Current measurement mode as as7331_measurement_mode_t 599 | */ 600 | as7331_measurement_mode_t getMeasurementMode() const; 601 | 602 | /** 603 | * @brief Check if power-down is enabled 604 | * 605 | * @return true if power-down is enabled, false otherwise 606 | */ 607 | bool isPowerDown() const; 608 | 609 | /** 610 | * @brief Check if standby mode is enabled 611 | * 612 | * @return true if standby is enabled, false otherwise 613 | */ 614 | bool isStandby() const; 615 | 616 | private: 617 | // Internal helper methods 618 | 619 | /** 620 | * @brief Set the device mode 621 | * 622 | * @param mode Device mode to set 623 | * @return true if successful, false otherwise 624 | */ 625 | bool setDeviceMode(const as7331_device_mode_t& mode); 626 | 627 | /** 628 | * @brief Read OSR register and update local config 629 | * 630 | * @param osr Struct to hold OSR register data 631 | * @return true if successful, false otherwise 632 | */ 633 | bool readOSRRegister(as7331_osr_reg_t& osr); 634 | 635 | /** 636 | * @brief Write OSR register and update local config 637 | * 638 | * @param osr OSR register data to write 639 | * @return true if successful, false otherwise 640 | */ 641 | bool writeOSRRegister(const as7331_osr_reg_t& osr); 642 | 643 | /** 644 | * @defgroup AS7331_I2C_Methods AS7331 Custom I2C Methods 645 | * @brief Custom I2C read and write functions for the AS7331 sensor. 646 | * 647 | * These functions utilize a "repeated start" condition instead of terminating each transaction with a STOP condition, as required by the AS7331. 648 | * This is achieved using `FuriHalI2cEndAwaitRestart` and `FuriHalI2cBeginRestart`, despite their documentation referring to "Clock Stretching," which the AS7331 does not support. 649 | * 650 | * @note The caller is responsible for calling `furi_hal_i2c_acquire` and `furi_hal_i2c_release`. 651 | * @{ 652 | */ 653 | 654 | /** 655 | * @brief Read multiple bytes from consecutive registers 656 | * 657 | * @param start_register_addr Starting register address 658 | * @param buffer Buffer to store read data 659 | * @param length Number of bytes to read 660 | * @return true if successful, false otherwise 661 | * @ingroup AS7331_I2C_Methods 662 | */ 663 | bool readRegisters(uint8_t start_register_addr, uint8_t* buffer, size_t length); 664 | 665 | /** 666 | * @brief Read an 8-bit register 667 | * 668 | * @param register_addr Register address to read 669 | * @param data Variable to store read data 670 | * @return true if successful, false otherwise 671 | * @ingroup AS7331_I2C_Methods 672 | */ 673 | bool readRegister(uint8_t register_addr, uint8_t& data); 674 | 675 | /** 676 | * @brief Read a 16-bit register 677 | * 678 | * @param register_addr Register address to read 679 | * @param data Variable to store read data 680 | * @return true if successful, false otherwise 681 | * @ingroup AS7331_I2C_Methods 682 | */ 683 | bool readRegister16(uint8_t register_addr, uint16_t& data); 684 | 685 | /** 686 | * @brief Write an 8-bit register 687 | * 688 | * @param register_addr Register address to write 689 | * @param data Data to write 690 | * @return true if successful, false otherwise 691 | * @ingroup AS7331_I2C_Methods 692 | */ 693 | bool writeRegister(uint8_t register_addr, const uint8_t& data); 694 | 695 | /** @} */ // End of AS7331_I2C_Methods group 696 | 697 | /** 698 | * @brief Scan I2C bus for AS7331 device 699 | * 700 | * @return Found I2C address in 7-bit format, 0 if not found 701 | */ 702 | uint8_t scan_i2c_bus(); 703 | 704 | /** 705 | * @brief Update local configuration variables from device registers 706 | * 707 | * @return true if successful, false otherwise 708 | */ 709 | bool updateLocalConfig(); 710 | 711 | /** 712 | * @brief Calculate Full-Scale Range (FSREe) for a given UV type 713 | * 714 | * @param uvType UV type (UV_A, UV_B, UV_C) 715 | * @param adjustForIntegrationTime Optional. A boolean flag indicating whether to adjust FSREe for the integration TIME setting. Default is `true`. 716 | * @return FSREe value in µW/cm², `-1.0` if an error occurs 717 | */ 718 | double calculateFSREe(as7331_uv_type_t uvType, bool adjustForIntegrationTime = true); 719 | 720 | // Private member variables 721 | uint8_t _i2c_addr_8bit; 722 | as7331_device_mode_t _deviceMode; 723 | bool _power_down; 724 | as7331_gain_t _gain; /**< Gain code (0 to 11) */ 725 | as7331_integration_time_t _integration_time; /**< Integration time code (0 to 15) */ 726 | bool _enable_divider; /**< Divider enabled (CREG2:EN_DIV) */ 727 | as7331_divider_t _divider; /**< Divider value (CREG2:DIV) */ 728 | as7331_clock_frequency_t _clock_frequency; /**< Clock frequency code (0 to 3) */ 729 | bool _standby; 730 | as7331_measurement_mode_t _measurement_mode; 731 | }; 732 | -------------------------------------------------------------------------------- /views/uv_meter_data.cpp: -------------------------------------------------------------------------------- 1 | #include "uv_meter_data.hpp" 2 | 3 | #include 4 | #include "uv_meter_as7331_icons.h" 5 | 6 | #include 7 | #include 8 | 9 | #define UV_METER_MAX_RAW_VALUE 65535.0 10 | 11 | struct UVMeterData { 12 | View* view; 13 | AS7331* sensor; 14 | FuriMutex* sensor_mutex; 15 | UVMeterDataEnterSettingsCallback callback; 16 | void* context; 17 | }; 18 | 19 | // Configuration mode states 20 | typedef enum { 21 | UVMeterConfigModeNone, // No config selected 22 | UVMeterConfigModeGain, // Configure gain 23 | UVMeterConfigModeExposureTime, // Configure exposure time 24 | UVMeterConfigModeEyesProtection, // Configure eyes protection mode 25 | } UVMeterConfigMode; 26 | 27 | typedef struct { 28 | FuriString* buffer; 29 | UVMeterConfigMode current_config_mode; 30 | AS7331::Results results; 31 | AS7331::RawResults raw_results; 32 | UVMeterEffectiveResults effective_results; 33 | int16_t gain_value; 34 | double conversion_time; 35 | bool eyes_protected; 36 | UVMeterUnit unit; 37 | } UVMeterDataModel; 38 | 39 | static void uv_meter_data_draw_raw_meter( 40 | Canvas* canvas, 41 | int x, 42 | int y, 43 | uint16_t raw_value, 44 | double max_meter_value) { 45 | int meter_value = (int)lround((double)raw_value / max_meter_value * (double)8.0); 46 | 47 | // Draw the meter frame 48 | canvas_draw_frame(canvas, x, y, 3, 10); 49 | 50 | // Draw the meter level 51 | canvas_draw_line(canvas, x + 1, y + 9, x + 1, y + 9 - meter_value); 52 | 53 | // Show alert if value is too low or at max 54 | if(raw_value < 5 || raw_value >= max_meter_value) { 55 | canvas_draw_icon(canvas, x + 5, y + 1, &I_Alert_9x8); 56 | } 57 | } 58 | 59 | static void uv_meter_data_draw_uv_measurements( 60 | Canvas* canvas, 61 | const AS7331::Results* results, 62 | const AS7331::RawResults* raw_results, 63 | UVMeterUnit unit, 64 | FuriString* buffer) { 65 | canvas_set_font(canvas, FontSecondary); 66 | int x_1_align = 0; 67 | int y_uva_bottom = 9; 68 | int y_uvb_bottom = 23; 69 | int y_uvc_bottom = 37; 70 | 71 | // Draw labels 72 | canvas_draw_str_aligned(canvas, x_1_align, y_uva_bottom, AlignLeft, AlignBottom, "UVA:"); 73 | canvas_draw_str_aligned(canvas, x_1_align, y_uvb_bottom, AlignLeft, AlignBottom, "UVB:"); 74 | canvas_draw_str_aligned(canvas, x_1_align, y_uvc_bottom, AlignLeft, AlignBottom, "UVC:"); 75 | 76 | // Display UV measurements 77 | canvas_set_font(canvas, FontPrimary); 78 | int x_2_align = 51; 79 | 80 | double multiplier = 1.0; 81 | switch(unit) { 82 | case UVMeterUnituW_cm_2: 83 | multiplier = 1.0; 84 | break; 85 | case UVMeterUnitW_m_2: 86 | multiplier = 0.01; 87 | break; 88 | case UVMeterUnitmW_m_2: 89 | multiplier = 10; 90 | break; 91 | } 92 | 93 | // Display UVA value 94 | double uv_a = results->uv_a * multiplier; 95 | furi_string_printf(buffer, "%.*f", (uv_a >= 1000 ? 0 : (uv_a >= 100 ? 1 : 2)), uv_a); 96 | canvas_draw_str_aligned( 97 | canvas, x_2_align, y_uva_bottom, AlignRight, AlignBottom, furi_string_get_cstr(buffer)); 98 | 99 | // Display UVB value 100 | double uv_b = results->uv_b * multiplier; 101 | furi_string_printf(buffer, "%.*f", (uv_b >= 1000 ? 0 : (uv_b >= 100 ? 1 : 2)), uv_b); 102 | canvas_draw_str_aligned( 103 | canvas, x_2_align, y_uvb_bottom, AlignRight, AlignBottom, furi_string_get_cstr(buffer)); 104 | 105 | // Display UVC value 106 | double uv_c = results->uv_c * multiplier; 107 | furi_string_printf(buffer, "%.*f", (uv_c >= 1000 ? 0 : (uv_c >= 100 ? 1 : 2)), uv_c); 108 | canvas_draw_str_aligned( 109 | canvas, x_2_align, y_uvc_bottom, AlignRight, AlignBottom, furi_string_get_cstr(buffer)); 110 | 111 | // Draw raw meters with alerts 112 | int raw_meter_x = x_2_align + 3; 113 | 114 | // UVA raw meter 115 | uv_meter_data_draw_raw_meter( 116 | canvas, raw_meter_x, y_uva_bottom - 9, raw_results->uv_a, UV_METER_MAX_RAW_VALUE); 117 | 118 | // UVB raw meter 119 | uv_meter_data_draw_raw_meter( 120 | canvas, raw_meter_x, y_uvb_bottom - 9, raw_results->uv_b, UV_METER_MAX_RAW_VALUE); 121 | 122 | // UVC raw meter 123 | uv_meter_data_draw_raw_meter( 124 | canvas, raw_meter_x, y_uvc_bottom - 9, raw_results->uv_c, UV_METER_MAX_RAW_VALUE); 125 | } 126 | 127 | static void uv_meter_data_draw_uv_effectiveness( 128 | Canvas* canvas, 129 | const UVMeterEffectiveResults* effective_results, 130 | FuriString* buffer) { 131 | canvas_set_font(canvas, FontSecondary); 132 | int x_3_align = 94; 133 | int y_uva_bottom = 9; 134 | int y_uvb_bottom = 23; 135 | int y_uvc_bottom = 37; 136 | 137 | // Skip if total is zero to avoid division by zero 138 | if(effective_results->uv_total_eff <= 0) { 139 | return; 140 | } 141 | 142 | // UVA percentage of Maximum Daily Exposure Time 143 | furi_string_printf( 144 | buffer, 145 | "%d%%", 146 | (int)lround(effective_results->uv_a_eff / effective_results->uv_total_eff * 100)); 147 | canvas_draw_str_aligned( 148 | canvas, x_3_align, y_uva_bottom, AlignRight, AlignBottom, furi_string_get_cstr(buffer)); 149 | 150 | // UVB percentage of Maximum Daily Exposure Time 151 | furi_string_printf( 152 | buffer, 153 | "%d%%", 154 | (int)lround(effective_results->uv_b_eff / effective_results->uv_total_eff * 100)); 155 | canvas_draw_str_aligned( 156 | canvas, x_3_align, y_uvb_bottom, AlignRight, AlignBottom, furi_string_get_cstr(buffer)); 157 | 158 | // UVC percentage of Maximum Daily Exposure Time 159 | furi_string_printf( 160 | buffer, 161 | "%d%%", 162 | (int)lround(effective_results->uv_c_eff / effective_results->uv_total_eff * 100)); 163 | canvas_draw_str_aligned( 164 | canvas, x_3_align, y_uvc_bottom, AlignRight, AlignBottom, furi_string_get_cstr(buffer)); 165 | 166 | // Draw separator line and arrow 167 | int x_3_4 = 96; 168 | canvas_draw_line(canvas, x_3_4, y_uva_bottom - 9, x_3_4, y_uvc_bottom); 169 | canvas_draw_icon(canvas, x_3_4 + 1, y_uvb_bottom - 6, &I_ButtonRightSmall_3x5); 170 | } 171 | 172 | static void uv_meter_data_draw_maximum_daily_exposure_time( 173 | Canvas* canvas, 174 | double t_max, 175 | FuriString* buffer) { 176 | int x_center_4_4 = 112; 177 | int y_uvb_bottom = 23; 178 | 179 | // Draw sun icon 180 | canvas_draw_icon(canvas, x_center_4_4 - 7, -7, &I_Sun_15x16); 181 | 182 | // Draw "min" label 183 | canvas_set_font(canvas, FontSecondary); 184 | canvas_draw_str_aligned(canvas, x_center_4_4, y_uvb_bottom + 2, AlignCenter, AlignTop, "min"); 185 | 186 | // Draw max exposure time in minutes 187 | canvas_set_font(canvas, FontPrimary); 188 | double t_max_minutes = t_max / 60; 189 | furi_string_printf( 190 | buffer, "%.*f", (t_max_minutes >= 100 ? 0 : (t_max_minutes >= 10 ? 1 : 2)), t_max_minutes); 191 | canvas_draw_str_aligned( 192 | canvas, x_center_4_4, y_uvb_bottom, AlignCenter, AlignBottom, furi_string_get_cstr(buffer)); 193 | } 194 | 195 | static void uv_meter_data_draw_config_section( 196 | Canvas* canvas, 197 | int16_t gain_value, 198 | double conversion_time, 199 | bool eyes_protected, 200 | UVMeterUnit unit, 201 | UVMeterConfigMode current_config_mode, 202 | FuriString* buffer) { 203 | // Draw unit 204 | const Icon* icon = NULL; 205 | switch(unit) { 206 | case UVMeterUnituW_cm_2: 207 | icon = &I_Unit_uW_cm2_34x11; 208 | break; 209 | case UVMeterUnitW_m_2: 210 | icon = &I_Unit_W_m2_22x11; 211 | break; 212 | case UVMeterUnitmW_m_2: 213 | icon = &I_Unit_mW_m2_28x11; 214 | break; 215 | } 216 | int x_1_align = 0; 217 | int y_conf = 52; 218 | canvas_draw_icon(canvas, x_1_align, y_conf - 11, icon); 219 | 220 | // Draw text explaining selected setting 221 | canvas_set_font(canvas, FontSecondary); 222 | int x_setting_right = 123; 223 | 224 | const char* setting_string; 225 | switch(current_config_mode) { 226 | case UVMeterConfigModeGain: 227 | setting_string = "Gain"; 228 | break; 229 | case UVMeterConfigModeExposureTime: 230 | setting_string = "Exposure Time (s)"; 231 | break; 232 | case UVMeterConfigModeEyesProtection: 233 | setting_string = "Eyes Protected"; 234 | break; 235 | default: 236 | setting_string = ""; 237 | break; 238 | } 239 | 240 | uint16_t text_width = canvas_string_width(canvas, setting_string); 241 | canvas_draw_str_aligned( 242 | canvas, x_setting_right, y_conf - 2, AlignRight, AlignBottom, setting_string); 243 | 244 | // Navigation arrows 245 | canvas_draw_icon(canvas, x_setting_right + 2, y_conf - 8, &I_ButtonRightSmall_3x5); 246 | canvas_draw_icon(canvas, x_setting_right - text_width - 5, y_conf - 8, &I_ButtonLeftSmall_3x5); 247 | 248 | // Frames/Boxes for settings 249 | int setting_x_size = 33; 250 | int setting_y_size = 14; 251 | 252 | // Config button box 253 | canvas_draw_rframe(canvas, 0, y_conf, setting_x_size, setting_y_size, 3); 254 | 255 | // Gain box 256 | if(current_config_mode == UVMeterConfigModeGain) { 257 | canvas_draw_rbox(canvas, setting_x_size - 1, y_conf, setting_x_size, setting_y_size, 3); 258 | } else { 259 | canvas_draw_rframe(canvas, setting_x_size - 1, y_conf, setting_x_size, setting_y_size, 3); 260 | } 261 | 262 | // Exposure Time box 263 | if(current_config_mode == UVMeterConfigModeExposureTime) { 264 | canvas_draw_rbox( 265 | canvas, setting_x_size * 2 - 2, y_conf, setting_x_size, setting_y_size, 3); 266 | } else { 267 | canvas_draw_rframe( 268 | canvas, setting_x_size * 2 - 2, y_conf, setting_x_size, setting_y_size, 3); 269 | } 270 | 271 | // Eyes Protection box 272 | if(current_config_mode == UVMeterConfigModeEyesProtection) { 273 | canvas_draw_rbox( 274 | canvas, setting_x_size * 3 - 3, y_conf, setting_x_size - 1, setting_y_size, 3); 275 | } else { 276 | canvas_draw_rframe( 277 | canvas, setting_x_size * 3 - 3, y_conf, setting_x_size - 1, setting_y_size, 3); 278 | } 279 | 280 | // Config button 281 | canvas_draw_icon(canvas, 2, y_conf + 3, &I_ButtonCenter_7x7); 282 | canvas_draw_str_aligned(canvas, 11, y_conf + 3, AlignLeft, AlignTop, "Conf"); 283 | 284 | // Gain value 285 | if(current_config_mode == UVMeterConfigModeGain) { 286 | canvas_set_color(canvas, ColorWhite); 287 | } 288 | furi_string_printf(buffer, "%d", gain_value); 289 | canvas_draw_str_aligned( 290 | canvas, 291 | setting_x_size - 1 + (setting_x_size / 2), 292 | y_conf + 3, 293 | AlignCenter, 294 | AlignTop, 295 | furi_string_get_cstr(buffer)); 296 | canvas_set_color(canvas, ColorBlack); 297 | 298 | // Exposure Time value 299 | if(current_config_mode == UVMeterConfigModeExposureTime) { 300 | canvas_set_color(canvas, ColorWhite); 301 | } 302 | furi_string_printf( 303 | buffer, 304 | "%.*f", 305 | (conversion_time >= 100 ? 1 : (conversion_time >= 10 ? 2 : 3)), 306 | conversion_time); 307 | canvas_draw_str_aligned( 308 | canvas, 309 | setting_x_size * 2 - 2 + (setting_x_size / 2), 310 | y_conf + 3, 311 | AlignCenter, 312 | AlignTop, 313 | furi_string_get_cstr(buffer)); 314 | canvas_set_color(canvas, ColorBlack); 315 | 316 | // Eyes Protection 317 | if(current_config_mode == UVMeterConfigModeEyesProtection) { 318 | canvas_set_color(canvas, ColorWhite); 319 | } 320 | canvas_draw_icon( 321 | canvas, 322 | setting_x_size * 3 - 3 + 4, 323 | y_conf + 2, 324 | (eyes_protected) ? &I_Sunglasses_24x8 : &I_Glasses_24x8); 325 | canvas_set_color(canvas, ColorBlack); 326 | } 327 | 328 | static void uv_meter_data_draw_callback(Canvas* canvas, void* model) { 329 | auto* m = static_cast(model); 330 | FURI_LOG_D("UV_Meter Data", "Redrawing"); 331 | 332 | // Draw UV measurements section 333 | uv_meter_data_draw_uv_measurements(canvas, &m->results, &m->raw_results, m->unit, m->buffer); 334 | 335 | // Draw UV effectiveness percentages 336 | uv_meter_data_draw_uv_effectiveness(canvas, &m->effective_results, m->buffer); 337 | 338 | // Draw maximum daily exposure time 339 | uv_meter_data_draw_maximum_daily_exposure_time(canvas, m->effective_results.t_max, m->buffer); 340 | 341 | // Draw configuration section 342 | uv_meter_data_draw_config_section( 343 | canvas, 344 | m->gain_value, 345 | m->conversion_time, 346 | m->eyes_protected, 347 | m->unit, 348 | m->current_config_mode, 349 | m->buffer); 350 | } 351 | 352 | static bool uv_meter_data_input_callback(InputEvent* event, void* context) { 353 | auto* instance = static_cast(context); 354 | 355 | bool consumed = false; 356 | bool setting_changed = false; 357 | 358 | if(event->type != InputTypeShort) { 359 | return false; 360 | } 361 | 362 | if(event->key == InputKeyOk) { 363 | if(instance->callback) { 364 | instance->callback(instance->context); 365 | return true; 366 | } 367 | return false; 368 | } 369 | FURI_LOG_D("UV_Meter Data", "Update Input"); 370 | with_view_model_cpp( 371 | instance->view, 372 | UVMeterDataModel*, 373 | model, 374 | { 375 | // Handle left/right to select config 376 | if(event->key == InputKeyLeft || event->key == InputKeyRight) { 377 | if(event->key == InputKeyLeft) { 378 | // Move to previous config mode 379 | switch(model->current_config_mode) { 380 | case UVMeterConfigModeNone: 381 | case UVMeterConfigModeGain: 382 | model->current_config_mode = UVMeterConfigModeEyesProtection; 383 | break; 384 | case UVMeterConfigModeExposureTime: 385 | model->current_config_mode = UVMeterConfigModeGain; 386 | break; 387 | case UVMeterConfigModeEyesProtection: 388 | model->current_config_mode = UVMeterConfigModeExposureTime; 389 | break; 390 | } 391 | } else { // InputKeyRight 392 | // Move to next config mode 393 | switch(model->current_config_mode) { 394 | case UVMeterConfigModeNone: 395 | case UVMeterConfigModeGain: 396 | model->current_config_mode = UVMeterConfigModeExposureTime; 397 | break; 398 | case UVMeterConfigModeExposureTime: 399 | model->current_config_mode = UVMeterConfigModeEyesProtection; 400 | break; 401 | case UVMeterConfigModeEyesProtection: 402 | model->current_config_mode = UVMeterConfigModeGain; 403 | break; 404 | } 405 | } 406 | consumed = true; 407 | setting_changed = true; 408 | } 409 | 410 | // Handle up/down to change selected config mode 411 | else if( 412 | instance->sensor && model->current_config_mode != UVMeterConfigModeNone && 413 | (event->key == InputKeyUp || event->key == InputKeyDown)) { 414 | furi_mutex_acquire(instance->sensor_mutex, FuriWaitForever); 415 | 416 | switch(model->current_config_mode) { 417 | case UVMeterConfigModeGain: { 418 | // Adjust gain 419 | int current_gain = static_cast(instance->sensor->getGain()); 420 | int new_gain = (event->key == InputKeyUp) ? current_gain - 1 : 421 | current_gain + 1; 422 | 423 | if(new_gain >= static_cast(GAIN_2048) && 424 | new_gain <= static_cast(GAIN_1)) { 425 | instance->sensor->setGain(static_cast(new_gain)); 426 | model->gain_value = instance->sensor->getGainValue(); 427 | setting_changed = true; 428 | } 429 | break; 430 | } 431 | 432 | case UVMeterConfigModeExposureTime: { 433 | // Adjust integration time 434 | int current_time = static_cast(instance->sensor->getIntegrationTime()); 435 | int new_time = (event->key == InputKeyUp) ? current_time + 1 : 436 | current_time - 1; 437 | 438 | if(new_time >= static_cast(TIME_1MS) && 439 | new_time <= static_cast(TIME_16384MS)) { 440 | instance->sensor->setIntegrationTime( 441 | static_cast(new_time)); 442 | model->conversion_time = instance->sensor->getConversionTime(); 443 | setting_changed = true; 444 | } 445 | break; 446 | } 447 | 448 | case UVMeterConfigModeEyesProtection: 449 | // Toggle eyes protected 450 | model->eyes_protected = !model->eyes_protected; 451 | model->effective_results = uv_meter_data_calculate_effective_results( 452 | &model->results, model->eyes_protected); 453 | setting_changed = true; 454 | break; 455 | 456 | default: 457 | break; 458 | } 459 | furi_mutex_release(instance->sensor_mutex); 460 | consumed = true; 461 | } 462 | }, 463 | setting_changed); 464 | 465 | return consumed; 466 | } 467 | 468 | UVMeterData* uv_meter_data_alloc(void) { 469 | UVMeterData* instance = new UVMeterData(); 470 | instance->view = view_alloc(); 471 | view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(UVMeterDataModel)); 472 | view_set_draw_callback(instance->view, uv_meter_data_draw_callback); 473 | view_set_input_callback(instance->view, uv_meter_data_input_callback); 474 | view_set_context(instance->view, instance); 475 | 476 | FURI_LOG_D("UV_Meter Data", "Update Alloc"); 477 | with_view_model_cpp( 478 | instance->view, 479 | UVMeterDataModel*, 480 | model, 481 | { 482 | model->buffer = furi_string_alloc(); 483 | model->current_config_mode = UVMeterConfigModeGain; 484 | model->results.uv_a = 0; 485 | model->results.uv_b = 0; 486 | model->results.uv_c = 0; 487 | model->raw_results.uv_a = 0; 488 | model->raw_results.uv_b = 0; 489 | model->raw_results.uv_c = 0; 490 | model->effective_results.uv_a_eff = 0; 491 | model->effective_results.uv_b_eff = 0; 492 | model->effective_results.uv_c_eff = 0; 493 | model->effective_results.uv_total_eff = 0; 494 | model->effective_results.t_max = 0; 495 | model->gain_value = 1; 496 | model->conversion_time = 1.0; 497 | model->eyes_protected = true; 498 | model->unit = UVMeterUnituW_cm_2; 499 | }, 500 | true); 501 | 502 | return instance; 503 | } 504 | 505 | void uv_meter_data_free(UVMeterData* instance) { 506 | furi_assert(instance); 507 | 508 | with_view_model_cpp( 509 | instance->view, UVMeterDataModel*, model, { furi_string_free(model->buffer); }, false); 510 | 511 | view_free(instance->view); 512 | delete instance; 513 | // Data View is not responsible for `sensor` and `sensor_mutex` 514 | } 515 | 516 | void uv_meter_data_reset(UVMeterData* instance, bool update) { 517 | furi_assert(instance); 518 | FURI_LOG_D("UV_Meter Data", "Update Reset"); 519 | with_view_model_cpp( 520 | instance->view, 521 | UVMeterDataModel*, 522 | model, 523 | { 524 | model->current_config_mode = UVMeterConfigModeGain; 525 | model->results.uv_a = 0; 526 | model->results.uv_b = 0; 527 | model->results.uv_c = 0; 528 | model->raw_results.uv_a = 0; 529 | model->raw_results.uv_b = 0; 530 | model->raw_results.uv_c = 0; 531 | model->effective_results.uv_a_eff = 0; 532 | model->effective_results.uv_b_eff = 0; 533 | model->effective_results.uv_c_eff = 0; 534 | model->effective_results.uv_total_eff = 0; 535 | model->effective_results.t_max = 0; 536 | model->gain_value = 1; 537 | model->conversion_time = 1.0; 538 | model->eyes_protected = true; 539 | model->unit = UVMeterUnituW_cm_2; 540 | }, 541 | update); 542 | } 543 | 544 | View* uv_meter_data_get_view(UVMeterData* instance) { 545 | furi_assert(instance); 546 | return instance->view; 547 | } 548 | 549 | void uv_meter_data_set_enter_settings_callback( 550 | UVMeterData* instance, 551 | UVMeterDataEnterSettingsCallback callback, 552 | void* context) { 553 | furi_assert(instance); 554 | furi_assert(callback); 555 | with_view_model_cpp( 556 | instance->view, 557 | UVMeterDataModel*, 558 | model, 559 | { 560 | UNUSED(model); 561 | instance->callback = callback; 562 | instance->context = context; 563 | }, 564 | false); 565 | } 566 | 567 | void uv_meter_data_set_sensor(UVMeterData* instance, AS7331* sensor, FuriMutex* sensor_mutex) { 568 | furi_assert(instance); 569 | furi_assert(sensor); 570 | furi_assert(sensor_mutex); 571 | with_view_model_cpp( 572 | instance->view, 573 | UVMeterDataModel*, 574 | model, 575 | { 576 | UNUSED(model); 577 | instance->sensor = sensor; 578 | instance->sensor_mutex = sensor_mutex; 579 | }, 580 | false); 581 | } 582 | 583 | void uv_meter_update_from_sensor(UVMeterData* instance) { 584 | furi_assert(instance); 585 | if(instance->sensor) { 586 | furi_mutex_acquire(instance->sensor_mutex, FuriWaitForever); 587 | FURI_LOG_D("UV_Meter Data", "Update From Sensor"); 588 | with_view_model_cpp( 589 | instance->view, 590 | UVMeterDataModel*, 591 | model, 592 | { 593 | model->gain_value = instance->sensor->getGainValue(); 594 | model->conversion_time = instance->sensor->getConversionTime(); 595 | }, 596 | true); 597 | furi_mutex_release(instance->sensor_mutex); 598 | } 599 | } 600 | 601 | void uv_meter_data_set_results( 602 | UVMeterData* instance, 603 | const AS7331::Results* results, 604 | const AS7331::RawResults* raw_results) { 605 | furi_assert(instance); 606 | furi_assert(results); 607 | furi_assert(raw_results); 608 | 609 | FURI_LOG_D("UV_Meter Data", "Update Set Results"); 610 | with_view_model_cpp( 611 | instance->view, 612 | UVMeterDataModel*, 613 | model, 614 | { 615 | model->results = *results; 616 | model->raw_results = *raw_results; 617 | model->effective_results = 618 | uv_meter_data_calculate_effective_results(&model->results, model->eyes_protected); 619 | }, 620 | true); 621 | } 622 | 623 | UVMeterEffectiveResults uv_meter_data_get_effective_results(UVMeterData* instance) { 624 | furi_assert(instance); 625 | UVMeterEffectiveResults effective_results; 626 | 627 | with_view_model_cpp( 628 | instance->view, 629 | UVMeterDataModel*, 630 | model, 631 | { effective_results = model->effective_results; }, 632 | false); 633 | 634 | return effective_results; 635 | } 636 | 637 | void uv_meter_data_set_eyes_protected(UVMeterData* instance, bool eyes_protected) { 638 | furi_assert(instance); 639 | FURI_LOG_D("UV_Meter Data", "Update Set Eyes Protected"); 640 | with_view_model_cpp( 641 | instance->view, 642 | UVMeterDataModel*, 643 | model, 644 | { model->eyes_protected = eyes_protected; }, 645 | true); 646 | } 647 | 648 | bool uv_meter_data_get_eyes_protected(UVMeterData* instance) { 649 | furi_assert(instance); 650 | bool eyes_protected = false; 651 | 652 | with_view_model_cpp( 653 | instance->view, 654 | UVMeterDataModel*, 655 | model, 656 | { eyes_protected = model->eyes_protected; }, 657 | false); 658 | 659 | return eyes_protected; 660 | } 661 | 662 | void uv_meter_data_set_unit(UVMeterData* instance, UVMeterUnit unit) { 663 | furi_assert(instance); 664 | FURI_LOG_D("UV_Meter Data", "Update Set Unit"); 665 | with_view_model_cpp(instance->view, UVMeterDataModel*, model, { model->unit = unit; }, true); 666 | } 667 | 668 | UVMeterEffectiveResults 669 | uv_meter_data_calculate_effective_results(const AS7331::Results* results, bool eyes_protected) { 670 | // Weighted Spectral Effectiveness 671 | double w_spectral_eff_uv_a = 0.0002824; 672 | double w_spectral_eff_uv_b = 0.3814; 673 | double w_spectral_eff_uv_c = 0.6047; 674 | 675 | if(eyes_protected) { // 😎 676 | // w_spectral_eff_uv_a is the same 677 | w_spectral_eff_uv_b = 0.2009; 678 | w_spectral_eff_uv_c = 0.2547; 679 | } 680 | UVMeterEffectiveResults effective_results; 681 | // Effective Irradiance 682 | effective_results.uv_a_eff = results->uv_a * w_spectral_eff_uv_a; 683 | effective_results.uv_b_eff = results->uv_b * w_spectral_eff_uv_b; 684 | effective_results.uv_c_eff = results->uv_c * w_spectral_eff_uv_c; 685 | effective_results.uv_total_eff = 686 | effective_results.uv_a_eff + effective_results.uv_b_eff + effective_results.uv_c_eff; 687 | 688 | // Daily dose (seconds) based on the total effective irradiance 689 | double daily_dose = 0.003; // J/cm^2 690 | double uW_to_W = 1e-6; 691 | effective_results.t_max = daily_dose / (effective_results.uv_total_eff * uW_to_W); 692 | return effective_results; 693 | } 694 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------