Merge remote-tracking branch 'origin/release-candidate' into release
							
								
								
									
										2
									
								
								.github/workflows/unit_tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -10,7 +10,7 @@ env: | ||||
| 
 | ||||
| jobs: | ||||
|   run_units_on_bench: | ||||
|     runs-on: [self-hosted, FlipperZeroTest] | ||||
|     runs-on: [self-hosted, FlipperZeroUnitTest] | ||||
|     steps: | ||||
|       - name: 'Decontaminate previous build leftovers' | ||||
|         run: | | ||||
|  | ||||
							
								
								
									
										4
									
								
								.github/workflows/updater_test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -10,7 +10,7 @@ env: | ||||
| 
 | ||||
| jobs: | ||||
|   test_updater_on_bench: | ||||
|     runs-on: [self-hosted, FlipperZeroTestMac1] | ||||
|     runs-on: [self-hosted, FlipperZeroUpdaterTest] | ||||
|     steps: | ||||
|       - name: 'Decontaminate previous build leftovers' | ||||
|         run: | | ||||
| @ -27,7 +27,7 @@ jobs: | ||||
|       - name: 'Get flipper from device manager (mock)' | ||||
|         id: device | ||||
|         run: | | ||||
|           echo "flipper=/dev/tty.usbmodemflip_Rekigyn1" >> $GITHUB_OUTPUT | ||||
|           echo "flipper=Rekigyn" >> $GITHUB_OUTPUT | ||||
|           echo "stlink=0F020D026415303030303032" >> $GITHUB_OUTPUT | ||||
| 
 | ||||
|       - name: 'Flashing target firmware' | ||||
|  | ||||
							
								
								
									
										5
									
								
								.vscode/example/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -11,9 +11,10 @@ | ||||
|             "args": { | ||||
|                 "useSingleResult": true, | ||||
|                 "env": { | ||||
|                     "PATH": "${workspaceFolder};${env:PATH}" | ||||
|                     "PATH": "${workspaceFolder};${env:PATH}", | ||||
|                     "FBT_QUIET": 1 | ||||
|                 }, | ||||
|                 "command": "./fbt get_blackmagic", | ||||
|                 "command": "fbt get_blackmagic", | ||||
|                 "description": "Get Blackmagic device", | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -3,56 +3,63 @@ | ||||
| #include "../minunit.h" | ||||
| 
 | ||||
| static void power_test_deinit(void) { | ||||
|     // Try to reset to default charging voltage
 | ||||
|     furi_hal_power_set_battery_charging_voltage(4.208f); | ||||
|     // Try to reset to default charge voltage limit
 | ||||
|     furi_hal_power_set_battery_charge_voltage_limit(4.208f); | ||||
| } | ||||
| 
 | ||||
| MU_TEST(test_power_charge_voltage_exact) { | ||||
|     // Power of 16mV charge voltages get applied exactly
 | ||||
| MU_TEST(test_power_charge_voltage_limit_exact) { | ||||
|     // Power of 16mV charge voltage limits get applied exactly
 | ||||
|     // (bq25896 charge controller works in 16mV increments)
 | ||||
|     //
 | ||||
|     // This test may need adapted if other charge controllers are used in the future.
 | ||||
|     for(uint16_t charge_mv = 3840; charge_mv <= 4208; charge_mv += 16) { | ||||
|         float charge_volt = (float)charge_mv / 1000.0f; | ||||
|         furi_hal_power_set_battery_charging_voltage(charge_volt); | ||||
|         mu_assert_double_eq(charge_volt, furi_hal_power_get_battery_charging_voltage()); | ||||
|         furi_hal_power_set_battery_charge_voltage_limit(charge_volt); | ||||
|         mu_assert_double_eq(charge_volt, furi_hal_power_get_battery_charge_voltage_limit()); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| MU_TEST(test_power_charge_voltage_floating_imprecision) { | ||||
| MU_TEST(test_power_charge_voltage_limit_floating_imprecision) { | ||||
|     // 4.016f should act as 4.016 V, even with floating point imprecision
 | ||||
|     furi_hal_power_set_battery_charging_voltage(4.016f); | ||||
|     mu_assert_double_eq(4.016f, furi_hal_power_get_battery_charging_voltage()); | ||||
|     furi_hal_power_set_battery_charge_voltage_limit(4.016f); | ||||
|     mu_assert_double_eq(4.016f, furi_hal_power_get_battery_charge_voltage_limit()); | ||||
| } | ||||
| 
 | ||||
| MU_TEST(test_power_charge_voltage_inexact) { | ||||
|     // Charge voltages that are not power of 16mV get truncated down
 | ||||
|     furi_hal_power_set_battery_charging_voltage(3.841f); | ||||
|     mu_assert_double_eq(3.840, furi_hal_power_get_battery_charging_voltage()); | ||||
| MU_TEST(test_power_charge_voltage_limit_inexact) { | ||||
|     // Charge voltage limits that are not power of 16mV get truncated down
 | ||||
|     furi_hal_power_set_battery_charge_voltage_limit(3.841f); | ||||
|     mu_assert_double_eq(3.840, furi_hal_power_get_battery_charge_voltage_limit()); | ||||
| 
 | ||||
|     furi_hal_power_set_battery_charging_voltage(3.900f); | ||||
|     mu_assert_double_eq(3.888, furi_hal_power_get_battery_charging_voltage()); | ||||
|     furi_hal_power_set_battery_charge_voltage_limit(3.900f); | ||||
|     mu_assert_double_eq(3.888, furi_hal_power_get_battery_charge_voltage_limit()); | ||||
| 
 | ||||
|     furi_hal_power_set_battery_charging_voltage(4.200f); | ||||
|     mu_assert_double_eq(4.192, furi_hal_power_get_battery_charging_voltage()); | ||||
|     furi_hal_power_set_battery_charge_voltage_limit(4.200f); | ||||
|     mu_assert_double_eq(4.192, furi_hal_power_get_battery_charge_voltage_limit()); | ||||
| } | ||||
| 
 | ||||
| MU_TEST(test_power_charge_voltage_invalid_clamped) { | ||||
|     // Out-of-range charge voltages get clamped to 3.840 V and 4.208 V
 | ||||
|     furi_hal_power_set_battery_charging_voltage(3.808f); | ||||
|     mu_assert_double_eq(3.840, furi_hal_power_get_battery_charging_voltage()); | ||||
| MU_TEST(test_power_charge_voltage_limit_invalid_clamped) { | ||||
|     // Out-of-range charge voltage limits get clamped to 3.840 V and 4.208 V
 | ||||
|     furi_hal_power_set_battery_charge_voltage_limit(3.808f); | ||||
|     mu_assert_double_eq(3.840, furi_hal_power_get_battery_charge_voltage_limit()); | ||||
|     furi_hal_power_set_battery_charge_voltage_limit(1.0f); | ||||
|     mu_assert_double_eq(3.840, furi_hal_power_get_battery_charge_voltage_limit()); | ||||
| 
 | ||||
|     // NOTE: Intentionally picking a small increment above 4.208 V to reduce the risk of an
 | ||||
|     // unhappy battery if this fails.
 | ||||
|     furi_hal_power_set_battery_charging_voltage(4.240f); | ||||
|     mu_assert_double_eq(4.208, furi_hal_power_get_battery_charging_voltage()); | ||||
|     furi_hal_power_set_battery_charge_voltage_limit(4.240f); | ||||
|     mu_assert_double_eq(4.208, furi_hal_power_get_battery_charge_voltage_limit()); | ||||
|     // Likewise, picking a number that the uint8_t wraparound in the driver would result in a
 | ||||
|     // VREG value under 23 if this test fails.
 | ||||
|     // E.g. (uint8_t)((8105-3840)/16) -> 10
 | ||||
|     furi_hal_power_set_battery_charge_voltage_limit(8.105f); | ||||
|     mu_assert_double_eq(4.208, furi_hal_power_get_battery_charge_voltage_limit()); | ||||
| } | ||||
| 
 | ||||
| MU_TEST_SUITE(test_power_suite) { | ||||
|     MU_RUN_TEST(test_power_charge_voltage_exact); | ||||
|     MU_RUN_TEST(test_power_charge_voltage_floating_imprecision); | ||||
|     MU_RUN_TEST(test_power_charge_voltage_inexact); | ||||
|     MU_RUN_TEST(test_power_charge_voltage_invalid_clamped); | ||||
|     MU_RUN_TEST(test_power_charge_voltage_limit_exact); | ||||
|     MU_RUN_TEST(test_power_charge_voltage_limit_floating_imprecision); | ||||
|     MU_RUN_TEST(test_power_charge_voltage_limit_inexact); | ||||
|     MU_RUN_TEST(test_power_charge_voltage_limit_invalid_clamped); | ||||
|     power_test_deinit(); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -142,10 +142,6 @@ void bad_usb_app_free(BadUsbApp* app) { | ||||
|         app->bad_usb_script = NULL; | ||||
|     } | ||||
| 
 | ||||
|     if(app->usb_if_prev) { | ||||
|         furi_check(furi_hal_usb_set_config(app->usb_if_prev, NULL)); | ||||
|     } | ||||
| 
 | ||||
|     // Views
 | ||||
|     view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewWork); | ||||
|     bad_usb_free(app->bad_usb_view); | ||||
| @ -172,6 +168,10 @@ void bad_usb_app_free(BadUsbApp* app) { | ||||
|     furi_string_free(app->file_path); | ||||
|     furi_string_free(app->keyboard_layout); | ||||
| 
 | ||||
|     if(app->usb_if_prev) { | ||||
|         furi_check(furi_hal_usb_set_config(app->usb_if_prev, NULL)); | ||||
|     } | ||||
| 
 | ||||
|     free(app); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -32,6 +32,7 @@ struct BadUsbScript { | ||||
|     FuriString* file_path; | ||||
|     uint32_t defdelay; | ||||
|     uint16_t layout[128]; | ||||
|     uint32_t stringdelay; | ||||
|     FuriThread* thread; | ||||
|     uint8_t file_buf[FILE_BUFFER_LEN + 1]; | ||||
|     uint8_t buf_start; | ||||
| @ -113,6 +114,8 @@ static const char ducky_cmd_delay[] = {"DELAY "}; | ||||
| static const char ducky_cmd_string[] = {"STRING "}; | ||||
| static const char ducky_cmd_defdelay_1[] = {"DEFAULT_DELAY "}; | ||||
| static const char ducky_cmd_defdelay_2[] = {"DEFAULTDELAY "}; | ||||
| static const char ducky_cmd_stringdelay_1[] = {"STRINGDELAY "}; | ||||
| static const char ducky_cmd_stringdelay_2[] = {"STRING_DELAY "}; | ||||
| static const char ducky_cmd_repeat[] = {"REPEAT "}; | ||||
| static const char ducky_cmd_sysrq[] = {"SYSRQ "}; | ||||
| 
 | ||||
| @ -211,14 +214,19 @@ static bool ducky_altstring(const char* param) { | ||||
| 
 | ||||
| static bool ducky_string(BadUsbScript* bad_usb, const char* param) { | ||||
|     uint32_t i = 0; | ||||
| 
 | ||||
|     while(param[i] != '\0') { | ||||
|         uint16_t keycode = BADUSB_ASCII_TO_KEY(bad_usb, param[i]); | ||||
|         if(keycode != HID_KEYBOARD_NONE) { | ||||
|             furi_hal_hid_kb_press(keycode); | ||||
|             furi_hal_hid_kb_release(keycode); | ||||
|             if(bad_usb->stringdelay > 0) { | ||||
|                 furi_delay_ms(bad_usb->stringdelay); | ||||
|             } | ||||
|         } | ||||
|         i++; | ||||
|     } | ||||
|     bad_usb->stringdelay = 0; | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| @ -277,6 +285,20 @@ static int32_t | ||||
|             snprintf(error, error_len, "Invalid number %s", line_tmp); | ||||
|         } | ||||
|         return (state) ? (0) : SCRIPT_STATE_ERROR; | ||||
|     } else if( | ||||
|         (strncmp(line_tmp, ducky_cmd_stringdelay_1, strlen(ducky_cmd_stringdelay_1)) == 0) || | ||||
|         (strncmp(line_tmp, ducky_cmd_stringdelay_2, strlen(ducky_cmd_stringdelay_2)) == 0)) { | ||||
|         //STRINGDELAY, finally it's here
 | ||||
|         line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1]; | ||||
|         state = ducky_get_number(line_tmp, &bad_usb->stringdelay); | ||||
|         if((state) && (bad_usb->stringdelay > 0)) { | ||||
|             return state; | ||||
|         } | ||||
|         if(error != NULL) { | ||||
|             snprintf(error, error_len, "Invalid number %s", line_tmp); | ||||
|         } | ||||
|         return SCRIPT_STATE_ERROR; | ||||
| 
 | ||||
|     } else if(strncmp(line_tmp, ducky_cmd_string, strlen(ducky_cmd_string)) == 0) { | ||||
|         // STRING
 | ||||
|         line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1]; | ||||
| @ -484,6 +506,19 @@ static void bad_usb_hid_state_callback(bool state, void* context) { | ||||
|         furi_thread_flags_set(furi_thread_get_id(bad_usb->thread), WorkerEvtDisconnect); | ||||
| } | ||||
| 
 | ||||
| static uint32_t bad_usb_flags_get(uint32_t flags_mask, uint32_t timeout) { | ||||
|     uint32_t flags = furi_thread_flags_get(); | ||||
|     furi_check((flags & FuriFlagError) == 0); | ||||
|     if(flags == 0) { | ||||
|         flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny, timeout); | ||||
|         furi_check(((flags & FuriFlagError) == 0) || (flags == (unsigned)FuriFlagErrorTimeout)); | ||||
|     } else { | ||||
|         uint32_t state = furi_thread_flags_clear(flags); | ||||
|         furi_check((state & FuriFlagError) == 0); | ||||
|     } | ||||
|     return flags; | ||||
| } | ||||
| 
 | ||||
| static int32_t bad_usb_worker(void* context) { | ||||
|     BadUsbScript* bad_usb = context; | ||||
| 
 | ||||
| @ -520,11 +555,9 @@ static int32_t bad_usb_worker(void* context) { | ||||
|             bad_usb->st.state = worker_state; | ||||
| 
 | ||||
|         } else if(worker_state == BadUsbStateNotConnected) { // State: USB not connected
 | ||||
|             uint32_t flags = furi_thread_flags_wait( | ||||
|                 WorkerEvtEnd | WorkerEvtConnect | WorkerEvtToggle, | ||||
|                 FuriFlagWaitAny, | ||||
|                 FuriWaitForever); | ||||
|             furi_check((flags & FuriFlagError) == 0); | ||||
|             uint32_t flags = bad_usb_flags_get( | ||||
|                 WorkerEvtEnd | WorkerEvtConnect | WorkerEvtToggle, FuriWaitForever); | ||||
| 
 | ||||
|             if(flags & WorkerEvtEnd) { | ||||
|                 break; | ||||
|             } else if(flags & WorkerEvtConnect) { | ||||
| @ -535,11 +568,9 @@ static int32_t bad_usb_worker(void* context) { | ||||
|             bad_usb->st.state = worker_state; | ||||
| 
 | ||||
|         } else if(worker_state == BadUsbStateIdle) { // State: ready to start
 | ||||
|             uint32_t flags = furi_thread_flags_wait( | ||||
|                 WorkerEvtEnd | WorkerEvtToggle | WorkerEvtDisconnect, | ||||
|                 FuriFlagWaitAny, | ||||
|                 FuriWaitForever); | ||||
|             furi_check((flags & FuriFlagError) == 0); | ||||
|             uint32_t flags = bad_usb_flags_get( | ||||
|                 WorkerEvtEnd | WorkerEvtToggle | WorkerEvtDisconnect, FuriWaitForever); | ||||
| 
 | ||||
|             if(flags & WorkerEvtEnd) { | ||||
|                 break; | ||||
|             } else if(flags & WorkerEvtToggle) { // Start executing script
 | ||||
| @ -548,6 +579,7 @@ static int32_t bad_usb_worker(void* context) { | ||||
|                 bad_usb->buf_len = 0; | ||||
|                 bad_usb->st.line_cur = 0; | ||||
|                 bad_usb->defdelay = 0; | ||||
|                 bad_usb->stringdelay = 0; | ||||
|                 bad_usb->repeat_cnt = 0; | ||||
|                 bad_usb->file_end = false; | ||||
|                 storage_file_seek(script_file, 0, true); | ||||
| @ -558,11 +590,9 @@ static int32_t bad_usb_worker(void* context) { | ||||
|             bad_usb->st.state = worker_state; | ||||
| 
 | ||||
|         } else if(worker_state == BadUsbStateWillRun) { // State: start on connection
 | ||||
|             uint32_t flags = furi_thread_flags_wait( | ||||
|                 WorkerEvtEnd | WorkerEvtConnect | WorkerEvtToggle, | ||||
|                 FuriFlagWaitAny, | ||||
|                 FuriWaitForever); | ||||
|             furi_check((flags & FuriFlagError) == 0); | ||||
|             uint32_t flags = bad_usb_flags_get( | ||||
|                 WorkerEvtEnd | WorkerEvtConnect | WorkerEvtToggle, FuriWaitForever); | ||||
| 
 | ||||
|             if(flags & WorkerEvtEnd) { | ||||
|                 break; | ||||
|             } else if(flags & WorkerEvtConnect) { // Start executing script
 | ||||
| @ -571,12 +601,22 @@ static int32_t bad_usb_worker(void* context) { | ||||
|                 bad_usb->buf_len = 0; | ||||
|                 bad_usb->st.line_cur = 0; | ||||
|                 bad_usb->defdelay = 0; | ||||
|                 bad_usb->stringdelay = 0; | ||||
|                 bad_usb->repeat_cnt = 0; | ||||
|                 bad_usb->file_end = false; | ||||
|                 storage_file_seek(script_file, 0, true); | ||||
|                 // extra time for PC to recognize Flipper as keyboard
 | ||||
|                 furi_thread_flags_wait(0, FuriFlagWaitAny, 1500); | ||||
|                 worker_state = BadUsbStateRunning; | ||||
|                 flags = furi_thread_flags_wait( | ||||
|                     WorkerEvtEnd | WorkerEvtDisconnect | WorkerEvtToggle, | ||||
|                     FuriFlagWaitAny | FuriFlagNoClear, | ||||
|                     1500); | ||||
|                 if(flags == (unsigned)FuriFlagErrorTimeout) { | ||||
|                     // If nothing happened - start script execution
 | ||||
|                     worker_state = BadUsbStateRunning; | ||||
|                 } else if(flags & WorkerEvtToggle) { | ||||
|                     worker_state = BadUsbStateIdle; | ||||
|                     furi_thread_flags_clear(WorkerEvtToggle); | ||||
|                 } | ||||
|             } else if(flags & WorkerEvtToggle) { // Cancel scheduled execution
 | ||||
|                 worker_state = BadUsbStateNotConnected; | ||||
|             } | ||||
| @ -586,6 +626,7 @@ static int32_t bad_usb_worker(void* context) { | ||||
|             uint16_t delay_cur = (delay_val > 1000) ? (1000) : (delay_val); | ||||
|             uint32_t flags = furi_thread_flags_wait( | ||||
|                 WorkerEvtEnd | WorkerEvtToggle | WorkerEvtDisconnect, FuriFlagWaitAny, delay_cur); | ||||
| 
 | ||||
|             delay_val -= delay_cur; | ||||
|             if(!(flags & FuriFlagError)) { | ||||
|                 if(flags & WorkerEvtEnd) { | ||||
| @ -629,9 +670,9 @@ static int32_t bad_usb_worker(void* context) { | ||||
|         } else if( | ||||
|             (worker_state == BadUsbStateFileError) || | ||||
|             (worker_state == BadUsbStateScriptError)) { // State: error
 | ||||
|             uint32_t flags = furi_thread_flags_wait( | ||||
|                 WorkerEvtEnd, FuriFlagWaitAny, FuriWaitForever); // Waiting for exit command
 | ||||
|             furi_check((flags & FuriFlagError) == 0); | ||||
|             uint32_t flags = | ||||
|                 bad_usb_flags_get(WorkerEvtEnd, FuriWaitForever); // Waiting for exit command
 | ||||
| 
 | ||||
|             if(flags & WorkerEvtEnd) { | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
| @ -22,7 +22,6 @@ static bool bad_usb_file_select(BadUsbApp* bad_usb) { | ||||
| void bad_usb_scene_file_select_on_enter(void* context) { | ||||
|     BadUsbApp* bad_usb = context; | ||||
| 
 | ||||
|     furi_hal_usb_disable(); | ||||
|     if(bad_usb->bad_usb_script) { | ||||
|         bad_usb_script_close(bad_usb->bad_usb_script); | ||||
|         bad_usb->bad_usb_script = NULL; | ||||
| @ -34,7 +33,6 @@ void bad_usb_scene_file_select_on_enter(void* context) { | ||||
| 
 | ||||
|         scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneWork); | ||||
|     } else { | ||||
|         furi_hal_usb_enable(); | ||||
|         view_dispatcher_stop(bad_usb->view_dispatcher); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -16,7 +16,9 @@ bool bad_usb_scene_work_on_event(void* context, SceneManagerEvent event) { | ||||
| 
 | ||||
|     if(event.type == SceneManagerEventTypeCustom) { | ||||
|         if(event.event == InputKeyLeft) { | ||||
|             scene_manager_next_scene(app->scene_manager, BadUsbSceneConfig); | ||||
|             if(bad_usb_is_idle_state(app->bad_usb_view)) { | ||||
|                 scene_manager_next_scene(app->scene_manager, BadUsbSceneConfig); | ||||
|             } | ||||
|             consumed = true; | ||||
|         } else if(event.event == InputKeyOk) { | ||||
|             bad_usb_script_toggle(app->bad_usb_script); | ||||
|  | ||||
| @ -48,17 +48,13 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) { | ||||
|     if((model->state.state == BadUsbStateIdle) || (model->state.state == BadUsbStateDone) || | ||||
|        (model->state.state == BadUsbStateNotConnected)) { | ||||
|         elements_button_center(canvas, "Run"); | ||||
|         elements_button_left(canvas, "Config"); | ||||
|     } else if((model->state.state == BadUsbStateRunning) || (model->state.state == BadUsbStateDelay)) { | ||||
|         elements_button_center(canvas, "Stop"); | ||||
|     } else if(model->state.state == BadUsbStateWillRun) { | ||||
|         elements_button_center(canvas, "Cancel"); | ||||
|     } | ||||
| 
 | ||||
|     if((model->state.state == BadUsbStateNotConnected) || | ||||
|        (model->state.state == BadUsbStateIdle) || (model->state.state == BadUsbStateDone)) { | ||||
|         elements_button_left(canvas, "Config"); | ||||
|     } | ||||
| 
 | ||||
|     if(model->state.state == BadUsbStateNotConnected) { | ||||
|         canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18); | ||||
|         canvas_set_font(canvas, FontPrimary); | ||||
| @ -203,6 +199,7 @@ void bad_usb_set_layout(BadUsb* bad_usb, const char* layout) { | ||||
|         { strlcpy(model->layout, layout, MAX_NAME_LEN); }, | ||||
|         true); | ||||
| } | ||||
| 
 | ||||
| void bad_usb_set_state(BadUsb* bad_usb, BadUsbState* st) { | ||||
|     furi_assert(st); | ||||
|     with_view_model( | ||||
| @ -214,3 +211,19 @@ void bad_usb_set_state(BadUsb* bad_usb, BadUsbState* st) { | ||||
|         }, | ||||
|         true); | ||||
| } | ||||
| 
 | ||||
| bool bad_usb_is_idle_state(BadUsb* bad_usb) { | ||||
|     bool is_idle = false; | ||||
|     with_view_model( | ||||
|         bad_usb->view, | ||||
|         BadUsbModel * model, | ||||
|         { | ||||
|             if((model->state.state == BadUsbStateIdle) || | ||||
|                (model->state.state == BadUsbStateDone) || | ||||
|                (model->state.state == BadUsbStateNotConnected)) { | ||||
|                 is_idle = true; | ||||
|             } | ||||
|         }, | ||||
|         false); | ||||
|     return is_idle; | ||||
| } | ||||
|  | ||||
| @ -19,3 +19,5 @@ void bad_usb_set_file_name(BadUsb* bad_usb, const char* name); | ||||
| void bad_usb_set_layout(BadUsb* bad_usb, const char* layout); | ||||
| 
 | ||||
| void bad_usb_set_state(BadUsb* bad_usb, BadUsbState* st); | ||||
| 
 | ||||
| bool bad_usb_is_idle_state(BadUsb* bad_usb); | ||||
|  | ||||
| @ -32,7 +32,7 @@ static void nfc_cli_detect(Cli* cli, FuriString* args) { | ||||
|     while(!cmd_exit) { | ||||
|         cmd_exit |= cli_cmd_interrupt_received(cli); | ||||
|         if(furi_hal_nfc_detect(&dev_data, 400)) { | ||||
|             printf("found: %s ", nfc_get_dev_type(dev_data.type)); | ||||
|             printf("Found: %s ", nfc_get_dev_type(dev_data.type)); | ||||
|             printf("UID length: %d, UID:", dev_data.uid_len); | ||||
|             for(size_t i = 0; i < dev_data.uid_len; i++) { | ||||
|                 printf("%02X", dev_data.uid[i]); | ||||
|  | ||||
| @ -29,6 +29,7 @@ ADD_SCENE(nfc, mf_desfire_menu, MfDesfireMenu) | ||||
| ADD_SCENE(nfc, mf_desfire_data, MfDesfireData) | ||||
| ADD_SCENE(nfc, mf_desfire_app, MfDesfireApp) | ||||
| ADD_SCENE(nfc, mf_classic_read_success, MfClassicReadSuccess) | ||||
| ADD_SCENE(nfc, mf_classic_data, MfClassicData) | ||||
| ADD_SCENE(nfc, mf_classic_menu, MfClassicMenu) | ||||
| ADD_SCENE(nfc, mf_classic_emulate, MfClassicEmulate) | ||||
| ADD_SCENE(nfc, mf_classic_keys, MfClassicKeys) | ||||
|  | ||||
							
								
								
									
										106
									
								
								applications/main/nfc/scenes/nfc_scene_mf_classic_data.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,106 @@ | ||||
| #include "../nfc_i.h" | ||||
| 
 | ||||
| void nfc_scene_mf_classic_data_on_enter(void* context) { | ||||
|     Nfc* nfc = context; | ||||
|     MfClassicType type = nfc->dev->dev_data.mf_classic_data.type; | ||||
|     MfClassicData* data = &nfc->dev->dev_data.mf_classic_data; | ||||
|     TextBox* text_box = nfc->text_box; | ||||
| 
 | ||||
|     text_box_set_font(text_box, TextBoxFontHex); | ||||
| 
 | ||||
|     int card_blocks = 0; | ||||
|     if(type == MfClassicType1k) { | ||||
|         card_blocks = MF_CLASSIC_1K_TOTAL_SECTORS_NUM * 4; | ||||
|     } else if(type == MfClassicType4k) { | ||||
|         // 16 sectors of 4 blocks each plus 8 sectors of 16 blocks each
 | ||||
|         card_blocks = MF_CLASSIC_1K_TOTAL_SECTORS_NUM * 4 + 8 * 16; | ||||
|     } else if(type == MfClassicTypeMini) { | ||||
|         card_blocks = MF_MINI_TOTAL_SECTORS_NUM * 4; | ||||
|     } | ||||
| 
 | ||||
|     int bytes_written = 0; | ||||
|     for(int block_num = 0; block_num < card_blocks; block_num++) { | ||||
|         bool is_sec_trailer = mf_classic_is_sector_trailer(block_num); | ||||
|         if(is_sec_trailer) { | ||||
|             uint8_t sector_num = mf_classic_get_sector_by_block(block_num); | ||||
|             MfClassicSectorTrailer* sec_tr = | ||||
|                 mf_classic_get_sector_trailer_by_sector(data, sector_num); | ||||
|             // Key A
 | ||||
|             for(size_t i = 0; i < sizeof(sec_tr->key_a); i += 2) { | ||||
|                 if((bytes_written % 8 == 0) && (bytes_written != 0)) { | ||||
|                     furi_string_push_back(nfc->text_box_store, '\n'); | ||||
|                 } | ||||
|                 if(mf_classic_is_key_found(data, sector_num, MfClassicKeyA)) { | ||||
|                     furi_string_cat_printf( | ||||
|                         nfc->text_box_store, "%02X%02X ", sec_tr->key_a[i], sec_tr->key_a[i + 1]); | ||||
|                 } else { | ||||
|                     furi_string_cat_printf(nfc->text_box_store, "???? "); | ||||
|                 } | ||||
|                 bytes_written += 2; | ||||
|             } | ||||
|             // Access bytes
 | ||||
|             for(size_t i = 0; i < MF_CLASSIC_ACCESS_BYTES_SIZE; i += 2) { | ||||
|                 if((bytes_written % 8 == 0) && (bytes_written != 0)) { | ||||
|                     furi_string_push_back(nfc->text_box_store, '\n'); | ||||
|                 } | ||||
|                 if(mf_classic_is_block_read(data, block_num)) { | ||||
|                     furi_string_cat_printf( | ||||
|                         nfc->text_box_store, | ||||
|                         "%02X%02X ", | ||||
|                         sec_tr->access_bits[i], | ||||
|                         sec_tr->access_bits[i + 1]); | ||||
|                 } else { | ||||
|                     furi_string_cat_printf(nfc->text_box_store, "???? "); | ||||
|                 } | ||||
|                 bytes_written += 2; | ||||
|             } | ||||
|             // Key B
 | ||||
|             for(size_t i = 0; i < sizeof(sec_tr->key_b); i += 2) { | ||||
|                 if((bytes_written % 8 == 0) && (bytes_written != 0)) { | ||||
|                     furi_string_push_back(nfc->text_box_store, '\n'); | ||||
|                 } | ||||
|                 if(mf_classic_is_key_found(data, sector_num, MfClassicKeyB)) { | ||||
|                     furi_string_cat_printf( | ||||
|                         nfc->text_box_store, "%02X%02X ", sec_tr->key_b[i], sec_tr->key_b[i + 1]); | ||||
|                 } else { | ||||
|                     furi_string_cat_printf(nfc->text_box_store, "???? "); | ||||
|                 } | ||||
|                 bytes_written += 2; | ||||
|             } | ||||
|         } else { | ||||
|             // Write data block
 | ||||
|             for(size_t i = 0; i < MF_CLASSIC_BLOCK_SIZE; i += 2) { | ||||
|                 if((bytes_written % 8 == 0) && (bytes_written != 0)) { | ||||
|                     furi_string_push_back(nfc->text_box_store, '\n'); | ||||
|                 } | ||||
|                 if(mf_classic_is_block_read(data, block_num)) { | ||||
|                     furi_string_cat_printf( | ||||
|                         nfc->text_box_store, | ||||
|                         "%02X%02X ", | ||||
|                         data->block[block_num].value[i], | ||||
|                         data->block[block_num].value[i + 1]); | ||||
|                 } else { | ||||
|                     furi_string_cat_printf(nfc->text_box_store, "???? "); | ||||
|                 } | ||||
|                 bytes_written += 2; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     text_box_set_text(text_box, furi_string_get_cstr(nfc->text_box_store)); | ||||
| 
 | ||||
|     view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewTextBox); | ||||
| } | ||||
| 
 | ||||
| bool nfc_scene_mf_classic_data_on_event(void* context, SceneManagerEvent event) { | ||||
|     UNUSED(context); | ||||
|     UNUSED(event); | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| void nfc_scene_mf_classic_data_on_exit(void* context) { | ||||
|     Nfc* nfc = context; | ||||
| 
 | ||||
|     // Clean view
 | ||||
|     text_box_reset(nfc->text_box); | ||||
|     furi_string_reset(nfc->text_box_store); | ||||
| } | ||||
| @ -14,7 +14,8 @@ void nfc_scene_nfc_data_info_on_enter(void* context) { | ||||
|     NfcDeviceData* dev_data = &nfc->dev->dev_data; | ||||
|     NfcProtocol protocol = dev_data->protocol; | ||||
|     uint8_t text_scroll_height = 0; | ||||
|     if((protocol == NfcDeviceProtocolMifareDesfire) || (protocol == NfcDeviceProtocolMifareUl)) { | ||||
|     if((protocol == NfcDeviceProtocolMifareDesfire) || (protocol == NfcDeviceProtocolMifareUl) || | ||||
|        (protocol == NfcDeviceProtocolMifareClassic)) { | ||||
|         widget_add_button_element( | ||||
|             widget, GuiButtonTypeRight, "More", nfc_scene_nfc_data_info_widget_callback, nfc); | ||||
|         text_scroll_height = 52; | ||||
| @ -136,6 +137,9 @@ bool nfc_scene_nfc_data_info_on_event(void* context, SceneManagerEvent event) { | ||||
|             } else if(protocol == NfcDeviceProtocolMifareUl) { | ||||
|                 scene_manager_next_scene(nfc->scene_manager, NfcSceneMfUltralightData); | ||||
|                 consumed = true; | ||||
|             } else if(protocol == NfcDeviceProtocolMifareClassic) { | ||||
|                 scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicData); | ||||
|                 consumed = true; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -150,6 +150,8 @@ bool nfc_scene_saved_menu_on_event(void* context, SceneManagerEvent event) { | ||||
|                 application_info_present = nfc_supported_card_verify_and_parse(dev_data); | ||||
|             } | ||||
| 
 | ||||
|             FURI_LOG_I("nfc", "application_info_present: %d", application_info_present); | ||||
| 
 | ||||
|             if(application_info_present) { | ||||
|                 scene_manager_next_scene(nfc->scene_manager, NfcSceneDeviceInfo); | ||||
|             } else { | ||||
|  | ||||
| @ -10,7 +10,7 @@ App( | ||||
|     stack_size=4 * 1024, | ||||
|     order=20, | ||||
|     fap_icon="dap_link.png", | ||||
|     fap_category="Tools", | ||||
|     fap_category="GPIO", | ||||
|     fap_private_libs=[ | ||||
|         Lib( | ||||
|             name="free-dap", | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| App( | ||||
|     appid="hid_usb", | ||||
|     name="USB Remote", | ||||
|     name="Remote", | ||||
|     apptype=FlipperAppType.PLUGIN, | ||||
|     entry_point="hid_usb_app", | ||||
|     stack_size=1 * 1024, | ||||
|     fap_category="Tools", | ||||
|     fap_category="USB", | ||||
|     fap_icon="hid_usb_10px.png", | ||||
|     fap_icon_assets="assets", | ||||
|     fap_icon_assets_symbol="hid", | ||||
| @ -13,11 +13,11 @@ App( | ||||
| 
 | ||||
| App( | ||||
|     appid="hid_ble", | ||||
|     name="Bluetooth Remote", | ||||
|     name="Remote", | ||||
|     apptype=FlipperAppType.PLUGIN, | ||||
|     entry_point="hid_ble_app", | ||||
|     stack_size=1 * 1024, | ||||
|     fap_category="Tools", | ||||
|     fap_category="Bluetooth", | ||||
|     fap_icon="hid_ble_10px.png", | ||||
|     fap_icon_assets="assets", | ||||
|     fap_icon_assets_symbol="hid", | ||||
|  | ||||
| @ -12,7 +12,7 @@ App( | ||||
|     stack_size=2 * 1024, | ||||
|     order=20, | ||||
|     fap_icon="icons/music_10px.png", | ||||
|     fap_category="Misc", | ||||
|     fap_category="Media", | ||||
|     fap_icon_assets="icons", | ||||
| ) | ||||
| 
 | ||||
|  | ||||
| @ -11,7 +11,7 @@ App( | ||||
|     stack_size=4 * 1024, | ||||
|     order=30, | ||||
|     fap_icon="../../../assets/icons/Archive/125_10px.png", | ||||
|     fap_category="Tools", | ||||
|     fap_category="NFC", | ||||
|     fap_private_libs=[ | ||||
|         Lib( | ||||
|             name="magic", | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| App( | ||||
|     appid="picopass", | ||||
|     name="PicoPass Reader", | ||||
|     name="PicoPass", | ||||
|     apptype=FlipperAppType.EXTERNAL, | ||||
|     targets=["f7"], | ||||
|     entry_point="picopass_app", | ||||
| @ -11,7 +11,7 @@ App( | ||||
|     stack_size=4 * 1024, | ||||
|     order=30, | ||||
|     fap_icon="125_10px.png", | ||||
|     fap_category="Tools", | ||||
|     fap_category="NFC", | ||||
|     fap_libs=["mbedtls"], | ||||
|     fap_private_libs=[ | ||||
|         Lib( | ||||
|  | ||||
| @ -171,6 +171,16 @@ void picopass_show_loading_popup(void* context, bool show) { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| bool picopass_is_memset(const uint8_t* data, const uint8_t pattern, size_t size) { | ||||
|     bool result = size > 0; | ||||
|     while(size > 0) { | ||||
|         result &= (*data == pattern); | ||||
|         data++; | ||||
|         size--; | ||||
|     } | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| int32_t picopass_app(void* p) { | ||||
|     UNUSED(p); | ||||
|     Picopass* picopass = picopass_alloc(); | ||||
|  | ||||
| @ -368,7 +368,7 @@ ReturnCode picopass_device_parse_wiegand(uint8_t* data, PicopassWiegandRecord* r | ||||
| 
 | ||||
|         record->CardNumber = (bot >> 1) & 0xFFFF; | ||||
|         record->FacilityCode = (bot >> 17) & 0xFF; | ||||
|         FURI_LOG_D(TAG, "FC:%u CN: %u\n", record->FacilityCode, record->CardNumber); | ||||
|         FURI_LOG_D(TAG, "FC: %u CN: %u", record->FacilityCode, record->CardNumber); | ||||
|         record->valid = true; | ||||
|     } else { | ||||
|         record->CardNumber = 0; | ||||
|  | ||||
| @ -22,6 +22,7 @@ | ||||
| #define PICOPASS_KD_BLOCK_INDEX 3 | ||||
| #define PICOPASS_KC_BLOCK_INDEX 4 | ||||
| #define PICOPASS_AIA_BLOCK_INDEX 5 | ||||
| #define PICOPASS_PACS_CFG_BLOCK_INDEX 6 | ||||
| 
 | ||||
| #define PICOPASS_APP_FOLDER ANY_PATH("picopass") | ||||
| #define PICOPASS_APP_EXTENSION ".picopass" | ||||
|  | ||||
| @ -81,3 +81,15 @@ void picopass_blink_start(Picopass* picopass); | ||||
| void picopass_blink_stop(Picopass* picopass); | ||||
| 
 | ||||
| void picopass_show_loading_popup(void* context, bool show); | ||||
| 
 | ||||
| /** Check if memory is set to pattern
 | ||||
|  * | ||||
|  * @warning    zero size will return false | ||||
|  * | ||||
|  * @param[in]  data     Pointer to the byte array | ||||
|  * @param[in]  pattern  The pattern | ||||
|  * @param[in]  size     The byte array size | ||||
|  * | ||||
|  * @return     True if memory is set to pattern, false otherwise | ||||
|  */ | ||||
| bool picopass_is_memset(const uint8_t* data, const uint8_t pattern, size_t size); | ||||
|  | ||||
| @ -5,7 +5,8 @@ | ||||
| #define TAG "PicopassWorker" | ||||
| 
 | ||||
| const uint8_t picopass_iclass_key[] = {0xaf, 0xa7, 0x85, 0xa7, 0xda, 0xb3, 0x33, 0x78}; | ||||
| const uint8_t picopass_factory_key[] = {0x76, 0x65, 0x54, 0x43, 0x32, 0x21, 0x10, 0x00}; | ||||
| const uint8_t picopass_factory_credit_key[] = {0x76, 0x65, 0x54, 0x43, 0x32, 0x21, 0x10, 0x00}; | ||||
| const uint8_t picopass_factory_debit_key[] = {0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87}; | ||||
| 
 | ||||
| static void picopass_worker_enable_field() { | ||||
|     furi_hal_nfc_ll_txrx_on(); | ||||
| @ -197,6 +198,28 @@ static ReturnCode picopass_auth_standard(uint8_t* csn, uint8_t* div_key) { | ||||
|     return rfalPicoPassPollerCheck(mac, &chkRes); | ||||
| } | ||||
| 
 | ||||
| static ReturnCode picopass_auth_factory(uint8_t* csn, uint8_t* div_key) { | ||||
|     rfalPicoPassReadCheckRes rcRes; | ||||
|     rfalPicoPassCheckRes chkRes; | ||||
| 
 | ||||
|     ReturnCode err; | ||||
| 
 | ||||
|     uint8_t mac[4] = {0}; | ||||
|     uint8_t ccnr[12] = {0}; | ||||
| 
 | ||||
|     err = rfalPicoPassPollerReadCheck(&rcRes); | ||||
|     if(err != ERR_NONE) { | ||||
|         FURI_LOG_E(TAG, "rfalPicoPassPollerReadCheck error %d", err); | ||||
|         return err; | ||||
|     } | ||||
|     memcpy(ccnr, rcRes.CCNR, sizeof(rcRes.CCNR)); // last 4 bytes left 0
 | ||||
| 
 | ||||
|     loclass_diversifyKey(csn, picopass_factory_debit_key, div_key); | ||||
|     loclass_opt_doReaderMAC(ccnr, div_key, mac); | ||||
| 
 | ||||
|     return rfalPicoPassPollerCheck(mac, &chkRes); | ||||
| } | ||||
| 
 | ||||
| static ReturnCode picopass_auth_dict( | ||||
|     uint8_t* csn, | ||||
|     PicopassPacs* pacs, | ||||
| @ -264,14 +287,23 @@ static ReturnCode picopass_auth_dict( | ||||
| ReturnCode picopass_auth(PicopassBlock* AA1, PicopassPacs* pacs) { | ||||
|     ReturnCode err; | ||||
| 
 | ||||
|     FURI_LOG_E(TAG, "Trying standard legacy key"); | ||||
|     FURI_LOG_I(TAG, "Trying standard legacy key"); | ||||
|     err = picopass_auth_standard( | ||||
|         AA1[PICOPASS_CSN_BLOCK_INDEX].data, AA1[PICOPASS_KD_BLOCK_INDEX].data); | ||||
|     if(err == ERR_NONE) { | ||||
|         memcpy(pacs->key, picopass_iclass_key, PICOPASS_BLOCK_LEN); | ||||
|         return ERR_NONE; | ||||
|     } | ||||
| 
 | ||||
|     FURI_LOG_E(TAG, "Starting user dictionary attack"); | ||||
|     FURI_LOG_I(TAG, "Trying factory default key"); | ||||
|     err = picopass_auth_factory( | ||||
|         AA1[PICOPASS_CSN_BLOCK_INDEX].data, AA1[PICOPASS_KD_BLOCK_INDEX].data); | ||||
|     if(err == ERR_NONE) { | ||||
|         memcpy(pacs->key, picopass_factory_debit_key, PICOPASS_BLOCK_LEN); | ||||
|         return ERR_NONE; | ||||
|     } | ||||
| 
 | ||||
|     FURI_LOG_I(TAG, "Starting user dictionary attack"); | ||||
|     err = picopass_auth_dict( | ||||
|         AA1[PICOPASS_CSN_BLOCK_INDEX].data, | ||||
|         pacs, | ||||
| @ -281,7 +313,7 @@ ReturnCode picopass_auth(PicopassBlock* AA1, PicopassPacs* pacs) { | ||||
|         return ERR_NONE; | ||||
|     } | ||||
| 
 | ||||
|     FURI_LOG_E(TAG, "Starting in-built dictionary attack"); | ||||
|     FURI_LOG_I(TAG, "Starting system dictionary attack"); | ||||
|     err = picopass_auth_dict( | ||||
|         AA1[PICOPASS_CSN_BLOCK_INDEX].data, | ||||
|         pacs, | ||||
| @ -406,6 +438,84 @@ ReturnCode picopass_write_card(PicopassBlock* AA1) { | ||||
|     return ERR_NONE; | ||||
| } | ||||
| 
 | ||||
| ReturnCode picopass_write_block(PicopassPacs* pacs, uint8_t blockNo, uint8_t* newBlock) { | ||||
|     rfalPicoPassIdentifyRes idRes; | ||||
|     rfalPicoPassSelectRes selRes; | ||||
|     rfalPicoPassReadCheckRes rcRes; | ||||
|     rfalPicoPassCheckRes chkRes; | ||||
| 
 | ||||
|     ReturnCode err; | ||||
| 
 | ||||
|     uint8_t div_key[8] = {0}; | ||||
|     uint8_t mac[4] = {0}; | ||||
|     uint8_t ccnr[12] = {0}; | ||||
| 
 | ||||
|     err = rfalPicoPassPollerIdentify(&idRes); | ||||
|     if(err != ERR_NONE) { | ||||
|         FURI_LOG_E(TAG, "rfalPicoPassPollerIdentify error %d", err); | ||||
|         return err; | ||||
|     } | ||||
| 
 | ||||
|     err = rfalPicoPassPollerSelect(idRes.CSN, &selRes); | ||||
|     if(err != ERR_NONE) { | ||||
|         FURI_LOG_E(TAG, "rfalPicoPassPollerSelect error %d", err); | ||||
|         return err; | ||||
|     } | ||||
| 
 | ||||
|     err = rfalPicoPassPollerReadCheck(&rcRes); | ||||
|     if(err != ERR_NONE) { | ||||
|         FURI_LOG_E(TAG, "rfalPicoPassPollerReadCheck error %d", err); | ||||
|         return err; | ||||
|     } | ||||
|     memcpy(ccnr, rcRes.CCNR, sizeof(rcRes.CCNR)); // last 4 bytes left 0
 | ||||
| 
 | ||||
|     loclass_diversifyKey(selRes.CSN, pacs->key, div_key); | ||||
|     loclass_opt_doReaderMAC(ccnr, div_key, mac); | ||||
| 
 | ||||
|     err = rfalPicoPassPollerCheck(mac, &chkRes); | ||||
|     if(err != ERR_NONE) { | ||||
|         FURI_LOG_E(TAG, "rfalPicoPassPollerCheck error %d", err); | ||||
|         return err; | ||||
|     } | ||||
| 
 | ||||
|     FURI_LOG_D(TAG, "rfalPicoPassPollerWriteBlock %d", blockNo); | ||||
|     uint8_t data[9] = { | ||||
|         blockNo, | ||||
|         newBlock[0], | ||||
|         newBlock[1], | ||||
|         newBlock[2], | ||||
|         newBlock[3], | ||||
|         newBlock[4], | ||||
|         newBlock[5], | ||||
|         newBlock[6], | ||||
|         newBlock[7]}; | ||||
|     loclass_doMAC_N(data, sizeof(data), div_key, mac); | ||||
|     FURI_LOG_D( | ||||
|         TAG, | ||||
|         "loclass_doMAC_N %d %02x%02x%02x%02x%02x%02x%02x%02x %02x%02x%02x%02x", | ||||
|         blockNo, | ||||
|         data[1], | ||||
|         data[2], | ||||
|         data[3], | ||||
|         data[4], | ||||
|         data[5], | ||||
|         data[6], | ||||
|         data[7], | ||||
|         data[8], | ||||
|         mac[0], | ||||
|         mac[1], | ||||
|         mac[2], | ||||
|         mac[3]); | ||||
| 
 | ||||
|     err = rfalPicoPassPollerWriteBlock(data[0], data + 1, mac); | ||||
|     if(err != ERR_NONE) { | ||||
|         FURI_LOG_E(TAG, "rfalPicoPassPollerWriteBlock error %d", err); | ||||
|         return err; | ||||
|     } | ||||
| 
 | ||||
|     return ERR_NONE; | ||||
| } | ||||
| 
 | ||||
| int32_t picopass_worker_task(void* context) { | ||||
|     PicopassWorker* picopass_worker = context; | ||||
| 
 | ||||
| @ -414,6 +524,8 @@ int32_t picopass_worker_task(void* context) { | ||||
|         picopass_worker_detect(picopass_worker); | ||||
|     } else if(picopass_worker->state == PicopassWorkerStateWrite) { | ||||
|         picopass_worker_write(picopass_worker); | ||||
|     } else if(picopass_worker->state == PicopassWorkerStateWriteStandardKey) { | ||||
|         picopass_worker_write_standard_key(picopass_worker); | ||||
|     } | ||||
|     picopass_worker_disable_field(ERR_NONE); | ||||
| 
 | ||||
| @ -448,7 +560,7 @@ void picopass_worker_detect(PicopassWorker* picopass_worker) { | ||||
|             } | ||||
| 
 | ||||
|             // Thank you proxmark!
 | ||||
|             pacs->legacy = (memcmp(AA1[5].data, "\xff\xff\xff\xff\xff\xff\xff\xff", 8) == 0); | ||||
|             pacs->legacy = picopass_is_memset(AA1[5].data, 0xFF, 8); | ||||
|             pacs->se_enabled = (memcmp(AA1[5].data, "\xff\xff\xff\x00\x06\xff\xff\xff", 8) == 0); | ||||
|             if(pacs->se_enabled) { | ||||
|                 FURI_LOG_D(TAG, "SE enabled"); | ||||
| @ -520,3 +632,46 @@ void picopass_worker_write(PicopassWorker* picopass_worker) { | ||||
|         furi_delay_ms(100); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void picopass_worker_write_standard_key(PicopassWorker* picopass_worker) { | ||||
|     PicopassDeviceData* dev_data = picopass_worker->dev_data; | ||||
|     PicopassBlock* AA1 = dev_data->AA1; | ||||
|     PicopassPacs* pacs = &dev_data->pacs; | ||||
|     ReturnCode err; | ||||
|     PicopassWorkerEvent nextState = PicopassWorkerEventSuccess; | ||||
| 
 | ||||
|     uint8_t* csn = AA1[PICOPASS_CSN_BLOCK_INDEX].data; | ||||
|     uint8_t* configBlock = AA1[PICOPASS_CONFIG_BLOCK_INDEX].data; | ||||
|     uint8_t fuses = configBlock[7]; | ||||
|     uint8_t* oldKey = AA1[PICOPASS_KD_BLOCK_INDEX].data; | ||||
| 
 | ||||
|     uint8_t newKey[PICOPASS_BLOCK_LEN] = {0}; | ||||
|     loclass_diversifyKey(csn, picopass_iclass_key, newKey); | ||||
| 
 | ||||
|     if((fuses & 0x80) == 0x80) { | ||||
|         FURI_LOG_D(TAG, "Plain write for personalized mode key change"); | ||||
|     } else { | ||||
|         FURI_LOG_D(TAG, "XOR write for application mode key change"); | ||||
|         // XOR when in application mode
 | ||||
|         for(size_t i = 0; i < PICOPASS_BLOCK_LEN; i++) { | ||||
|             newKey[i] ^= oldKey[i]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     while(picopass_worker->state == PicopassWorkerStateWriteStandardKey) { | ||||
|         if(picopass_detect_card(1000) == ERR_NONE) { | ||||
|             err = picopass_write_block(pacs, PICOPASS_KD_BLOCK_INDEX, newKey); | ||||
|             if(err != ERR_NONE) { | ||||
|                 FURI_LOG_E(TAG, "picopass_write_block error %d", err); | ||||
|                 nextState = PicopassWorkerEventFail; | ||||
|             } | ||||
| 
 | ||||
|             // Notify caller and exit
 | ||||
|             if(picopass_worker->callback) { | ||||
|                 picopass_worker->callback(nextState, picopass_worker->context); | ||||
|             } | ||||
|             break; | ||||
|         } | ||||
|         furi_delay_ms(100); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -12,6 +12,7 @@ typedef enum { | ||||
|     // Main worker states
 | ||||
|     PicopassWorkerStateDetect, | ||||
|     PicopassWorkerStateWrite, | ||||
|     PicopassWorkerStateWriteStandardKey, | ||||
|     // Transition
 | ||||
|     PicopassWorkerStateStop, | ||||
| } PicopassWorkerState; | ||||
|  | ||||
| @ -31,3 +31,4 @@ int32_t picopass_worker_task(void* context); | ||||
| 
 | ||||
| void picopass_worker_detect(PicopassWorker* picopass_worker); | ||||
| void picopass_worker_write(PicopassWorker* picopass_worker); | ||||
| void picopass_worker_write_standard_key(PicopassWorker* picopass_worker); | ||||
|  | ||||
| @ -11,3 +11,5 @@ ADD_SCENE(picopass, delete, Delete) | ||||
| ADD_SCENE(picopass, delete_success, DeleteSuccess) | ||||
| ADD_SCENE(picopass, write_card, WriteCard) | ||||
| ADD_SCENE(picopass, write_card_success, WriteCardSuccess) | ||||
| ADD_SCENE(picopass, read_factory_success, ReadFactorySuccess) | ||||
| ADD_SCENE(picopass, write_key, WriteKey) | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| #include "../picopass_i.h" | ||||
| #include <dolphin/dolphin.h> | ||||
| 
 | ||||
| const uint8_t picopass_factory_key_check[] = {0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87}; | ||||
| 
 | ||||
| void picopass_read_card_worker_callback(PicopassWorkerEvent event, void* context) { | ||||
|     UNUSED(event); | ||||
|     Picopass* picopass = context; | ||||
| @ -34,7 +36,14 @@ bool picopass_scene_read_card_on_event(void* context, SceneManagerEvent event) { | ||||
| 
 | ||||
|     if(event.type == SceneManagerEventTypeCustom) { | ||||
|         if(event.event == PicopassCustomEventWorkerExit) { | ||||
|             scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadCardSuccess); | ||||
|             if(memcmp( | ||||
|                    picopass->dev->dev_data.pacs.key, | ||||
|                    picopass_factory_key_check, | ||||
|                    PICOPASS_BLOCK_LEN) == 0) { | ||||
|                 scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadFactorySuccess); | ||||
|             } else { | ||||
|                 scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadCardSuccess); | ||||
|             } | ||||
|             consumed = true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -15,6 +15,7 @@ void picopass_scene_read_card_success_widget_callback( | ||||
| 
 | ||||
| void picopass_scene_read_card_success_on_enter(void* context) { | ||||
|     Picopass* picopass = context; | ||||
| 
 | ||||
|     FuriString* csn_str = furi_string_alloc_set("CSN:"); | ||||
|     FuriString* credential_str = furi_string_alloc(); | ||||
|     FuriString* wiegand_str = furi_string_alloc(); | ||||
| @ -30,27 +31,31 @@ void picopass_scene_read_card_success_on_enter(void* context) { | ||||
|     PicopassPacs* pacs = &picopass->dev->dev_data.pacs; | ||||
|     Widget* widget = picopass->widget; | ||||
| 
 | ||||
|     uint8_t csn[PICOPASS_BLOCK_LEN]; | ||||
|     memcpy(csn, &AA1->data[PICOPASS_CSN_BLOCK_INDEX], PICOPASS_BLOCK_LEN); | ||||
|     uint8_t csn[PICOPASS_BLOCK_LEN] = {0}; | ||||
|     memcpy(csn, AA1[PICOPASS_CSN_BLOCK_INDEX].data, PICOPASS_BLOCK_LEN); | ||||
|     for(uint8_t i = 0; i < PICOPASS_BLOCK_LEN; i++) { | ||||
|         furi_string_cat_printf(csn_str, "%02X ", csn[i]); | ||||
|     } | ||||
| 
 | ||||
|     // Neither of these are valid.  Indicates the block was all 0x00 or all 0xff
 | ||||
|     if(pacs->record.bitLength == 0 || pacs->record.bitLength == 255) { | ||||
|     bool no_key = picopass_is_memset(pacs->key, 0x00, PICOPASS_BLOCK_LEN); | ||||
|     bool empty = | ||||
|         picopass_is_memset(AA1[PICOPASS_PACS_CFG_BLOCK_INDEX].data, 0xFF, PICOPASS_BLOCK_LEN); | ||||
| 
 | ||||
|     if(no_key) { | ||||
|         furi_string_cat_printf(wiegand_str, "Read Failed"); | ||||
| 
 | ||||
|         if(pacs->se_enabled) { | ||||
|             furi_string_cat_printf(credential_str, "SE enabled"); | ||||
|         } | ||||
|     } else if(empty) { | ||||
|         furi_string_cat_printf(wiegand_str, "Empty"); | ||||
|     } else if(pacs->record.bitLength == 0 || pacs->record.bitLength == 255) { | ||||
|         // Neither of these are valid.  Indicates the block was all 0x00 or all 0xff
 | ||||
|         furi_string_cat_printf(wiegand_str, "Invalid PACS"); | ||||
| 
 | ||||
|         widget_add_button_element( | ||||
|             widget, | ||||
|             GuiButtonTypeLeft, | ||||
|             "Retry", | ||||
|             picopass_scene_read_card_success_widget_callback, | ||||
|             picopass); | ||||
| 
 | ||||
|         if(pacs->se_enabled) { | ||||
|             furi_string_cat_printf(credential_str, "SE enabled"); | ||||
|         } | ||||
|     } else { | ||||
|         size_t bytesLength = 1 + pacs->record.bitLength / 8; | ||||
|         furi_string_set(credential_str, ""); | ||||
| @ -82,13 +87,6 @@ void picopass_scene_read_card_success_on_enter(void* context) { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         widget_add_button_element( | ||||
|             widget, | ||||
|             GuiButtonTypeLeft, | ||||
|             "Retry", | ||||
|             picopass_scene_read_card_success_widget_callback, | ||||
|             picopass); | ||||
| 
 | ||||
|         widget_add_button_element( | ||||
|             widget, | ||||
|             GuiButtonTypeRight, | ||||
| @ -97,6 +95,13 @@ void picopass_scene_read_card_success_on_enter(void* context) { | ||||
|             picopass); | ||||
|     } | ||||
| 
 | ||||
|     widget_add_button_element( | ||||
|         widget, | ||||
|         GuiButtonTypeLeft, | ||||
|         "Retry", | ||||
|         picopass_scene_read_card_success_widget_callback, | ||||
|         picopass); | ||||
| 
 | ||||
|     widget_add_string_element( | ||||
|         widget, 64, 5, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(csn_str)); | ||||
|     widget_add_string_element( | ||||
|  | ||||
| @ -0,0 +1,78 @@ | ||||
| #include "../picopass_i.h" | ||||
| #include <dolphin/dolphin.h> | ||||
| 
 | ||||
| void picopass_scene_read_factory_success_widget_callback( | ||||
|     GuiButtonType result, | ||||
|     InputType type, | ||||
|     void* context) { | ||||
|     furi_assert(context); | ||||
|     Picopass* picopass = context; | ||||
| 
 | ||||
|     if(type == InputTypeShort) { | ||||
|         view_dispatcher_send_custom_event(picopass->view_dispatcher, result); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void picopass_scene_read_factory_success_on_enter(void* context) { | ||||
|     Picopass* picopass = context; | ||||
|     FuriString* title = furi_string_alloc_set("Factory Default"); | ||||
|     FuriString* subtitle = furi_string_alloc_set(""); | ||||
| 
 | ||||
|     DOLPHIN_DEED(DolphinDeedNfcReadSuccess); | ||||
| 
 | ||||
|     // Send notification
 | ||||
|     notification_message(picopass->notifications, &sequence_success); | ||||
| 
 | ||||
|     // Setup view
 | ||||
|     Widget* widget = picopass->widget; | ||||
|     //PicopassPacs* pacs = &picopass->dev->dev_data.pacs;
 | ||||
|     PicopassBlock* AA1 = picopass->dev->dev_data.AA1; | ||||
| 
 | ||||
|     uint8_t* configBlock = AA1[PICOPASS_CONFIG_BLOCK_INDEX].data; | ||||
|     uint8_t fuses = configBlock[7]; | ||||
| 
 | ||||
|     if((fuses & 0x80) == 0x80) { | ||||
|         furi_string_cat_printf(subtitle, "Personalization mode"); | ||||
|     } else { | ||||
|         furi_string_cat_printf(subtitle, "Application mode"); | ||||
|     } | ||||
| 
 | ||||
|     widget_add_button_element( | ||||
|         widget, | ||||
|         GuiButtonTypeCenter, | ||||
|         "Write Standard iClass Key", | ||||
|         picopass_scene_read_factory_success_widget_callback, | ||||
|         picopass); | ||||
| 
 | ||||
|     widget_add_string_element( | ||||
|         widget, 64, 5, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(title)); | ||||
|     widget_add_string_element( | ||||
|         widget, 64, 20, AlignCenter, AlignCenter, FontPrimary, furi_string_get_cstr(subtitle)); | ||||
| 
 | ||||
|     furi_string_free(title); | ||||
|     furi_string_free(subtitle); | ||||
| 
 | ||||
|     view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget); | ||||
| } | ||||
| 
 | ||||
| bool picopass_scene_read_factory_success_on_event(void* context, SceneManagerEvent event) { | ||||
|     Picopass* picopass = context; | ||||
|     bool consumed = false; | ||||
| 
 | ||||
|     if(event.type == SceneManagerEventTypeCustom) { | ||||
|         if(event.event == GuiButtonTypeLeft) { | ||||
|             consumed = scene_manager_previous_scene(picopass->scene_manager); | ||||
|         } else if(event.event == GuiButtonTypeCenter) { | ||||
|             scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteKey); | ||||
|             consumed = true; | ||||
|         } | ||||
|     } | ||||
|     return consumed; | ||||
| } | ||||
| 
 | ||||
| void picopass_scene_read_factory_success_on_exit(void* context) { | ||||
|     Picopass* picopass = context; | ||||
| 
 | ||||
|     // Clear view
 | ||||
|     widget_reset(picopass->widget); | ||||
| } | ||||
| @ -16,6 +16,7 @@ void picopass_scene_write_card_success_widget_callback( | ||||
| void picopass_scene_write_card_success_on_enter(void* context) { | ||||
|     Picopass* picopass = context; | ||||
|     Widget* widget = picopass->widget; | ||||
|     FuriString* str = furi_string_alloc_set("Write Success!"); | ||||
| 
 | ||||
|     DOLPHIN_DEED(DolphinDeedNfcReadSuccess); | ||||
| 
 | ||||
| @ -29,6 +30,18 @@ void picopass_scene_write_card_success_on_enter(void* context) { | ||||
|         picopass_scene_write_card_success_widget_callback, | ||||
|         picopass); | ||||
| 
 | ||||
|     widget_add_button_element( | ||||
|         widget, | ||||
|         GuiButtonTypeRight, | ||||
|         "Menu", | ||||
|         picopass_scene_write_card_success_widget_callback, | ||||
|         picopass); | ||||
| 
 | ||||
|     widget_add_string_element( | ||||
|         widget, 64, 5, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(str)); | ||||
| 
 | ||||
|     furi_string_free(str); | ||||
| 
 | ||||
|     view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -0,0 +1,53 @@ | ||||
| #include "../picopass_i.h" | ||||
| #include <dolphin/dolphin.h> | ||||
| 
 | ||||
| void picopass_write_key_worker_callback(PicopassWorkerEvent event, void* context) { | ||||
|     UNUSED(event); | ||||
|     Picopass* picopass = context; | ||||
|     view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventWorkerExit); | ||||
| } | ||||
| 
 | ||||
| void picopass_scene_write_key_on_enter(void* context) { | ||||
|     Picopass* picopass = context; | ||||
|     DOLPHIN_DEED(DolphinDeedNfcSave); | ||||
| 
 | ||||
|     // Setup view
 | ||||
|     Popup* popup = picopass->popup; | ||||
|     popup_set_header(popup, "Writing\niClass\nkey", 68, 30, AlignLeft, AlignTop); | ||||
|     popup_set_icon(popup, 0, 3, &I_RFIDDolphinSend_97x61); | ||||
| 
 | ||||
|     // Start worker
 | ||||
|     view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup); | ||||
|     picopass_worker_start( | ||||
|         picopass->worker, | ||||
|         PicopassWorkerStateWriteStandardKey, | ||||
|         &picopass->dev->dev_data, | ||||
|         picopass_write_key_worker_callback, | ||||
|         picopass); | ||||
| 
 | ||||
|     picopass_blink_start(picopass); | ||||
| } | ||||
| 
 | ||||
| bool picopass_scene_write_key_on_event(void* context, SceneManagerEvent event) { | ||||
|     Picopass* picopass = context; | ||||
|     bool consumed = false; | ||||
| 
 | ||||
|     if(event.type == SceneManagerEventTypeCustom) { | ||||
|         if(event.event == PicopassCustomEventWorkerExit) { | ||||
|             scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteCardSuccess); | ||||
|             consumed = true; | ||||
|         } | ||||
|     } | ||||
|     return consumed; | ||||
| } | ||||
| 
 | ||||
| void picopass_scene_write_key_on_exit(void* context) { | ||||
|     Picopass* picopass = context; | ||||
| 
 | ||||
|     // Stop worker
 | ||||
|     picopass_worker_stop(picopass->worker); | ||||
|     // Clear view
 | ||||
|     popup_reset(picopass->popup); | ||||
| 
 | ||||
|     picopass_blink_stop(picopass); | ||||
| } | ||||
| @ -8,6 +8,6 @@ App( | ||||
|     stack_size=1 * 1024, | ||||
|     order=50, | ||||
|     fap_icon="signal_gen_10px.png", | ||||
|     fap_category="Tools", | ||||
|     fap_category="GPIO", | ||||
|     fap_icon_assets="icons", | ||||
| ) | ||||
|  | ||||
| @ -7,7 +7,7 @@ App( | ||||
|     stack_size=1 * 2048, | ||||
|     order=30, | ||||
|     fap_icon="images/Dip8_10px.png", | ||||
|     fap_category="Tools", | ||||
|     fap_category="GPIO", | ||||
|     fap_icon_assets="images", | ||||
|     fap_private_libs=[ | ||||
|         Lib( | ||||
|  | ||||
| @ -9,6 +9,6 @@ App( | ||||
|     stack_size=4 * 1024, | ||||
|     order=50, | ||||
|     fap_icon="weather_station_10px.png", | ||||
|     fap_category="Tools", | ||||
|     fap_category="Sub-GHz", | ||||
|     fap_icon_assets="images", | ||||
| ) | ||||
|  | ||||
| @ -195,6 +195,10 @@ bool weather_station_scene_receiver_on_event(void* context, SceneManagerEvent ev | ||||
|             ws_hopper_update(app); | ||||
|             weather_station_scene_receiver_update_statusbar(app); | ||||
|         } | ||||
|         // Get current RSSI
 | ||||
|         float rssi = furi_hal_subghz_get_rssi(); | ||||
|         ws_view_receiver_set_rssi(app->ws_receiver, rssi); | ||||
| 
 | ||||
|         if(app->txrx->txrx_state == WSTxRxStateRx) { | ||||
|             notification_message(app->notifications, &sequence_blink_cyan_10); | ||||
|         } | ||||
|  | ||||
| @ -12,6 +12,7 @@ | ||||
| #define MENU_ITEMS 4u | ||||
| #define UNLOCK_CNT 3 | ||||
| 
 | ||||
| #define SUBGHZ_RAW_TRESHOLD_MIN -90.0f | ||||
| typedef struct { | ||||
|     FuriString* item_str; | ||||
|     uint8_t type; | ||||
| @ -59,8 +60,24 @@ typedef struct { | ||||
|     uint16_t list_offset; | ||||
|     uint16_t history_item; | ||||
|     WSReceiverBarShow bar_show; | ||||
|     uint8_t u_rssi; | ||||
| } WSReceiverModel; | ||||
| 
 | ||||
| void ws_view_receiver_set_rssi(WSReceiver* instance, float rssi) { | ||||
|     furi_assert(instance); | ||||
|     with_view_model( | ||||
|         instance->view, | ||||
|         WSReceiverModel * model, | ||||
|         { | ||||
|             if(rssi < SUBGHZ_RAW_TRESHOLD_MIN) { | ||||
|                 model->u_rssi = 0; | ||||
|             } else { | ||||
|                 model->u_rssi = (uint8_t)(rssi - SUBGHZ_RAW_TRESHOLD_MIN); | ||||
|             } | ||||
|         }, | ||||
|         true); | ||||
| } | ||||
| 
 | ||||
| void ws_view_receiver_set_lock(WSReceiver* ws_receiver, WSLock lock) { | ||||
|     furi_assert(ws_receiver); | ||||
|     ws_receiver->lock_count = 0; | ||||
| @ -164,13 +181,22 @@ static void ws_view_receiver_draw_frame(Canvas* canvas, uint16_t idx, bool scrol | ||||
|     canvas_draw_dot(canvas, scrollbar ? 121 : 126, (0 + idx * FRAME_HEIGHT) + 11); | ||||
| } | ||||
| 
 | ||||
| static void ws_view_rssi_draw(Canvas* canvas, WSReceiverModel* model) { | ||||
|     for(uint8_t i = 1; i < model->u_rssi; i++) { | ||||
|         if(i % 5) { | ||||
|             canvas_draw_dot(canvas, 46 + i, 50); | ||||
|             canvas_draw_dot(canvas, 47 + i, 51); | ||||
|             canvas_draw_dot(canvas, 46 + i, 52); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void ws_view_receiver_draw(Canvas* canvas, WSReceiverModel* model) { | ||||
|     canvas_clear(canvas); | ||||
|     canvas_set_color(canvas, ColorBlack); | ||||
|     canvas_set_font(canvas, FontSecondary); | ||||
| 
 | ||||
|     elements_button_left(canvas, "Config"); | ||||
|     canvas_draw_line(canvas, 46, 51, 125, 51); | ||||
| 
 | ||||
|     bool scrollbar = model->history_item > 4; | ||||
|     FuriString* str_buff; | ||||
| @ -203,10 +229,12 @@ void ws_view_receiver_draw(Canvas* canvas, WSReceiverModel* model) { | ||||
|         canvas_draw_icon(canvas, 0, 0, &I_Scanning_123x52); | ||||
|         canvas_set_font(canvas, FontPrimary); | ||||
|         canvas_draw_str(canvas, 63, 46, "Scanning..."); | ||||
|         canvas_draw_line(canvas, 46, 51, 125, 51); | ||||
|         canvas_set_font(canvas, FontSecondary); | ||||
|     } | ||||
| 
 | ||||
|     // Draw RSSI
 | ||||
|     ws_view_rssi_draw(canvas, model); | ||||
| 
 | ||||
|     switch(model->bar_show) { | ||||
|     case WSReceiverBarShowLock: | ||||
|         canvas_draw_icon(canvas, 64, 55, &I_Lock_7x8); | ||||
|  | ||||
| @ -8,6 +8,8 @@ typedef struct WSReceiver WSReceiver; | ||||
| 
 | ||||
| typedef void (*WSReceiverCallback)(WSCustomEvent event, void* context); | ||||
| 
 | ||||
| void ws_view_receiver_set_rssi(WSReceiver* instance, float rssi); | ||||
| 
 | ||||
| void ws_view_receiver_set_lock(WSReceiver* ws_receiver, WSLock keyboard); | ||||
| 
 | ||||
| void ws_view_receiver_set_callback( | ||||
|  | ||||
| @ -373,7 +373,7 @@ int32_t bt_srv(void* p) { | ||||
|     Bt* bt = bt_alloc(); | ||||
| 
 | ||||
|     if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) { | ||||
|         FURI_LOG_W(TAG, "Skipped BT init: device in special startup mode"); | ||||
|         FURI_LOG_W(TAG, "Skipping start in special boot mode"); | ||||
|         ble_glue_wait_for_c2_start(FURI_HAL_BT_C2_START_TIMEOUT); | ||||
|         furi_record_create(RECORD_BT, bt); | ||||
|         return 0; | ||||
|  | ||||
| @ -461,7 +461,7 @@ int32_t cli_srv(void* p) { | ||||
|     if(furi_hal_rtc_get_boot_mode() == FuriHalRtcBootModeNormal) { | ||||
|         cli_session_open(cli, &cli_vcp); | ||||
|     } else { | ||||
|         FURI_LOG_W(TAG, "Skipped CLI session open: device in special startup mode"); | ||||
|         FURI_LOG_W(TAG, "Skipping start in special boot mode"); | ||||
|     } | ||||
| 
 | ||||
|     while(1) { | ||||
|  | ||||
| @ -12,26 +12,48 @@ | ||||
| // Close to ISO, `date +'%Y-%m-%d %H:%M:%S %u'`
 | ||||
| #define CLI_DATE_FORMAT "%.4d-%.2d-%.2d %.2d:%.2d:%.2d %d" | ||||
| 
 | ||||
| void cli_command_device_info_callback(const char* key, const char* value, bool last, void* context) { | ||||
|     UNUSED(context); | ||||
| void cli_command_info_callback(const char* key, const char* value, bool last, void* context) { | ||||
|     UNUSED(last); | ||||
|     UNUSED(context); | ||||
|     printf("%-30s: %s\r\n", key, value); | ||||
| } | ||||
| 
 | ||||
| /* 
 | ||||
|  * Device Info Command | ||||
| /** Info Command
 | ||||
|  * | ||||
|  * This command is intended to be used by humans | ||||
|  * | ||||
|  * Arguments: | ||||
|  * - device - print device info | ||||
|  * - power - print power info | ||||
|  * - power_debug - print power debug info | ||||
|  * | ||||
|  * @param      cli      The cli instance | ||||
|  * @param      args     The arguments | ||||
|  * @param      context  The context | ||||
|  */ | ||||
| void cli_command_device_info(Cli* cli, FuriString* args, void* context) { | ||||
| void cli_command_info(Cli* cli, FuriString* args, void* context) { | ||||
|     UNUSED(cli); | ||||
|     UNUSED(args); | ||||
|     furi_hal_info_get(cli_command_device_info_callback, '_', context); | ||||
| 
 | ||||
|     if(context) { | ||||
|         furi_hal_info_get(cli_command_info_callback, '_', NULL); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if(!furi_string_cmp(args, "device")) { | ||||
|         furi_hal_info_get(cli_command_info_callback, '.', NULL); | ||||
|     } else if(!furi_string_cmp(args, "power")) { | ||||
|         furi_hal_power_info_get(cli_command_info_callback, '.', NULL); | ||||
|     } else if(!furi_string_cmp(args, "power_debug")) { | ||||
|         furi_hal_power_debug_get(cli_command_info_callback, NULL); | ||||
|     } else { | ||||
|         cli_print_usage("info", "<device|power|power_debug>", furi_string_get_cstr(args)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void cli_command_help(Cli* cli, FuriString* args, void* context) { | ||||
|     UNUSED(args); | ||||
|     UNUSED(context); | ||||
|     printf("Commands we have:"); | ||||
|     printf("Commands available:"); | ||||
| 
 | ||||
|     // Command count
 | ||||
|     const size_t commands_count = CliCommandTree_size(cli->commands); | ||||
| @ -61,9 +83,9 @@ void cli_command_help(Cli* cli, FuriString* args, void* context) { | ||||
| 
 | ||||
|     if(furi_string_size(args) > 0) { | ||||
|         cli_nl(); | ||||
|         printf("Also I have no clue what '"); | ||||
|         printf("`"); | ||||
|         printf("%s", furi_string_get_cstr(args)); | ||||
|         printf("' is."); | ||||
|         printf("` command not found"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -410,8 +432,9 @@ void cli_command_i2c(Cli* cli, FuriString* args, void* context) { | ||||
| } | ||||
| 
 | ||||
| void cli_commands_init(Cli* cli) { | ||||
|     cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_device_info, NULL); | ||||
|     cli_add_command(cli, "device_info", CliCommandFlagParallelSafe, cli_command_device_info, NULL); | ||||
|     cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); | ||||
|     cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); | ||||
|     cli_add_command(cli, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true); | ||||
| 
 | ||||
|     cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL); | ||||
|     cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL); | ||||
|  | ||||
| @ -17,6 +17,8 @@ | ||||
| #include "helpers/pin_lock.h" | ||||
| #include "helpers/slideshow_filename.h" | ||||
| 
 | ||||
| #define TAG "Desktop" | ||||
| 
 | ||||
| static void desktop_auto_lock_arm(Desktop*); | ||||
| static void desktop_auto_lock_inhibit(Desktop*); | ||||
| static void desktop_start_auto_lock_timer(Desktop*); | ||||
| @ -321,6 +323,12 @@ static bool desktop_check_file_flag(const char* flag_path) { | ||||
| 
 | ||||
| int32_t desktop_srv(void* p) { | ||||
|     UNUSED(p); | ||||
| 
 | ||||
|     if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) { | ||||
|         FURI_LOG_W(TAG, "Skipping start in special boot mode"); | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     Desktop* desktop = desktop_alloc(); | ||||
| 
 | ||||
|     bool loaded = DESKTOP_SETTINGS_LOAD(&desktop->settings); | ||||
|  | ||||
| @ -154,6 +154,12 @@ static void dolphin_update_clear_limits_timer_period(Dolphin* dolphin) { | ||||
| 
 | ||||
| int32_t dolphin_srv(void* p) { | ||||
|     UNUSED(p); | ||||
| 
 | ||||
|     if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) { | ||||
|         FURI_LOG_W(TAG, "Skipping start in special boot mode"); | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     Dolphin* dolphin = dolphin_alloc(); | ||||
|     furi_record_create(RECORD_DOLPHIN, dolphin); | ||||
| 
 | ||||
|  | ||||
| @ -26,24 +26,6 @@ void power_cli_reboot2dfu(Cli* cli, FuriString* args) { | ||||
|     power_reboot(PowerBootModeDfu); | ||||
| } | ||||
| 
 | ||||
| static void power_cli_callback(const char* key, const char* value, bool last, void* context) { | ||||
|     UNUSED(last); | ||||
|     UNUSED(context); | ||||
|     printf("%-24s: %s\r\n", key, value); | ||||
| } | ||||
| 
 | ||||
| void power_cli_info(Cli* cli, FuriString* args) { | ||||
|     UNUSED(cli); | ||||
|     UNUSED(args); | ||||
|     furi_hal_power_info_get(power_cli_callback, '_', NULL); | ||||
| } | ||||
| 
 | ||||
| void power_cli_debug(Cli* cli, FuriString* args) { | ||||
|     UNUSED(cli); | ||||
|     UNUSED(args); | ||||
|     furi_hal_power_debug_get(power_cli_callback, NULL); | ||||
| } | ||||
| 
 | ||||
| void power_cli_5v(Cli* cli, FuriString* args) { | ||||
|     UNUSED(cli); | ||||
|     if(!furi_string_cmp(args, "0")) { | ||||
| @ -74,8 +56,6 @@ static void power_cli_command_print_usage() { | ||||
|     printf("\toff\t - shutdown power\r\n"); | ||||
|     printf("\treboot\t - reboot\r\n"); | ||||
|     printf("\treboot2dfu\t - reboot to dfu bootloader\r\n"); | ||||
|     printf("\tinfo\t - show power info\r\n"); | ||||
|     printf("\tdebug\t - show debug information\r\n"); | ||||
|     printf("\t5v <0 or 1>\t - enable or disable 5v ext\r\n"); | ||||
|     if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { | ||||
|         printf("\t3v3 <0 or 1>\t - enable or disable 3v3 ext\r\n"); | ||||
| @ -108,16 +88,6 @@ void power_cli(Cli* cli, FuriString* args, void* context) { | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         if(furi_string_cmp_str(cmd, "info") == 0) { | ||||
|             power_cli_info(cli, args); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         if(furi_string_cmp_str(cmd, "debug") == 0) { | ||||
|             power_cli_debug(cli, args); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         if(furi_string_cmp_str(cmd, "5v") == 0) { | ||||
|             power_cli_5v(cli, args); | ||||
|             break; | ||||
|  | ||||
| @ -4,6 +4,7 @@ | ||||
| #include <furi_hal.h> | ||||
| 
 | ||||
| #define POWER_OFF_TIMEOUT 90 | ||||
| #define TAG "Power" | ||||
| 
 | ||||
| void power_draw_battery_callback(Canvas* canvas, void* context) { | ||||
|     furi_assert(context); | ||||
| @ -12,8 +13,8 @@ void power_draw_battery_callback(Canvas* canvas, void* context) { | ||||
| 
 | ||||
|     if(power->info.gauge_is_ok) { | ||||
|         canvas_draw_box(canvas, 2, 2, (power->info.charge + 4) / 5, 4); | ||||
|         if(power->info.voltage_battery_charging < 4.2) { | ||||
|             // Battery charging voltage is modified, indicate with cross pattern
 | ||||
|         if(power->info.voltage_battery_charge_limit < 4.2) { | ||||
|             // Battery charge voltage limit is modified, indicate with cross pattern
 | ||||
|             canvas_invert_color(canvas); | ||||
|             uint8_t battery_bar_width = (power->info.charge + 4) / 5; | ||||
|             bool cross_odd = false; | ||||
| @ -146,7 +147,7 @@ static bool power_update_info(Power* power) { | ||||
|     info.capacity_full = furi_hal_power_get_battery_full_capacity(); | ||||
|     info.current_charger = furi_hal_power_get_battery_current(FuriHalPowerICCharger); | ||||
|     info.current_gauge = furi_hal_power_get_battery_current(FuriHalPowerICFuelGauge); | ||||
|     info.voltage_battery_charging = furi_hal_power_get_battery_charging_voltage(); | ||||
|     info.voltage_battery_charge_limit = furi_hal_power_get_battery_charge_voltage_limit(); | ||||
|     info.voltage_charger = furi_hal_power_get_battery_voltage(FuriHalPowerICCharger); | ||||
|     info.voltage_gauge = furi_hal_power_get_battery_voltage(FuriHalPowerICFuelGauge); | ||||
|     info.voltage_vbus = furi_hal_power_get_usb_voltage(); | ||||
| @ -217,6 +218,12 @@ static void power_check_battery_level_change(Power* power) { | ||||
| 
 | ||||
| int32_t power_srv(void* p) { | ||||
|     UNUSED(p); | ||||
| 
 | ||||
|     if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) { | ||||
|         FURI_LOG_W(TAG, "Skipping start in special boot mode"); | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     Power* power = power_alloc(); | ||||
|     power_update_info(power); | ||||
|     furi_record_create(RECORD_POWER, power); | ||||
|  | ||||
| @ -41,7 +41,7 @@ typedef struct { | ||||
|     float current_charger; | ||||
|     float current_gauge; | ||||
| 
 | ||||
|     float voltage_battery_charging; | ||||
|     float voltage_battery_charge_limit; | ||||
|     float voltage_charger; | ||||
|     float voltage_gauge; | ||||
|     float voltage_vbus; | ||||
|  | ||||
| @ -7,7 +7,7 @@ static void power_settings_scene_battery_info_update_model(PowerSettingsApp* app | ||||
|         .gauge_voltage = app->info.voltage_gauge, | ||||
|         .gauge_current = app->info.current_gauge, | ||||
|         .gauge_temperature = app->info.temperature_gauge, | ||||
|         .charging_voltage = app->info.voltage_battery_charging, | ||||
|         .charge_voltage_limit = app->info.voltage_battery_charge_limit, | ||||
|         .charge = app->info.charge, | ||||
|         .health = app->info.health, | ||||
|     }; | ||||
|  | ||||
| @ -69,7 +69,7 @@ static void draw_battery(Canvas* canvas, BatteryInfoModel* data, int x, int y) { | ||||
|             drain_current > HIGH_DRAIN_CURRENT_THRESHOLD ? "mA!" : "mA"); | ||||
|     } else if(drain_current != 0) { | ||||
|         snprintf(header, 20, "..."); | ||||
|     } else if(data->charging_voltage < 4.2) { | ||||
|     } else if(data->charge_voltage_limit < 4.2) { | ||||
|         // Non-default battery charging limit, mention it
 | ||||
|         snprintf(emote, sizeof(emote), "Charged!"); | ||||
|         snprintf(header, sizeof(header), "Limited to"); | ||||
| @ -77,8 +77,8 @@ static void draw_battery(Canvas* canvas, BatteryInfoModel* data, int x, int y) { | ||||
|             value, | ||||
|             sizeof(value), | ||||
|             "%lu.%luV", | ||||
|             (uint32_t)(data->charging_voltage), | ||||
|             (uint32_t)(data->charging_voltage * 10) % 10); | ||||
|             (uint32_t)(data->charge_voltage_limit), | ||||
|             (uint32_t)(data->charge_voltage_limit * 10) % 10); | ||||
|     } else { | ||||
|         snprintf(header, sizeof(header), "Charged!"); | ||||
|     } | ||||
|  | ||||
| @ -9,7 +9,7 @@ typedef struct { | ||||
|     float gauge_voltage; | ||||
|     float gauge_current; | ||||
|     float gauge_temperature; | ||||
|     float charging_voltage; | ||||
|     float charge_voltage_limit; | ||||
|     uint8_t charge; | ||||
|     uint8_t health; | ||||
| } BatteryInfoModel; | ||||
|  | ||||
| @ -103,6 +103,9 @@ static void storage_settings_scene_benchmark(StorageSettings* app) { | ||||
|             break; | ||||
| 
 | ||||
|         furi_string_cat_printf(app->text_string, "R %luK", bench_r_speed[i]); | ||||
| 
 | ||||
|         storage_common_remove(app->fs_api, BENCH_FILE); | ||||
| 
 | ||||
|         dialog_ex_set_text( | ||||
|             dialog_ex, furi_string_get_cstr(app->text_string), 0, 32, AlignLeft, AlignCenter); | ||||
|     } | ||||
|  | ||||
| @ -97,7 +97,16 @@ static void update_task_cleanup_resources(UpdateTask* update_task, const uint32_ | ||||
|                 path_concat( | ||||
|                     STORAGE_EXT_PATH_PREFIX, furi_string_get_cstr(entry_ptr->name), file_path); | ||||
|                 FURI_LOG_D(TAG, "Removing %s", furi_string_get_cstr(file_path)); | ||||
|                 storage_simply_remove(update_task->storage, furi_string_get_cstr(file_path)); | ||||
| 
 | ||||
|                 FS_Error result = | ||||
|                     storage_common_remove(update_task->storage, furi_string_get_cstr(file_path)); | ||||
|                 if(result != FSE_OK && result != FSE_EXIST) { | ||||
|                     FURI_LOG_E( | ||||
|                         TAG, | ||||
|                         "%s remove failed, cause %s", | ||||
|                         furi_string_get_cstr(file_path), | ||||
|                         storage_error_get_desc(result)); | ||||
|                 } | ||||
|                 furi_string_free(file_path); | ||||
|             } else if(entry_ptr->type == ResourceManifestEntryTypeDirectory) { | ||||
|                 n_dir_entries++; | ||||
| @ -116,7 +125,6 @@ static void update_task_cleanup_resources(UpdateTask* update_task, const uint32_ | ||||
|                             n_dir_entries); | ||||
| 
 | ||||
|                 FuriString* folder_path = furi_string_alloc(); | ||||
|                 File* folder_file = storage_file_alloc(update_task->storage); | ||||
| 
 | ||||
|                 do { | ||||
|                     path_concat( | ||||
| @ -125,24 +133,17 @@ static void update_task_cleanup_resources(UpdateTask* update_task, const uint32_ | ||||
|                         folder_path); | ||||
| 
 | ||||
|                     FURI_LOG_D(TAG, "Removing folder %s", furi_string_get_cstr(folder_path)); | ||||
|                     if(!storage_dir_open(folder_file, furi_string_get_cstr(folder_path))) { | ||||
|                         FURI_LOG_W( | ||||
|                     FS_Error result = storage_common_remove( | ||||
|                         update_task->storage, furi_string_get_cstr(folder_path)); | ||||
|                     if(result != FSE_OK && result != FSE_EXIST) { | ||||
|                         FURI_LOG_E( | ||||
|                             TAG, | ||||
|                             "%s can't be opened, skipping", | ||||
|                             furi_string_get_cstr(folder_path)); | ||||
|                         break; | ||||
|                             "%s remove failed, cause %s", | ||||
|                             furi_string_get_cstr(folder_path), | ||||
|                             storage_error_get_desc(result)); | ||||
|                     } | ||||
| 
 | ||||
|                     if(storage_dir_read(folder_file, NULL, NULL, 0)) { | ||||
|                         FURI_LOG_I( | ||||
|                             TAG, "%s is not empty, skipping", furi_string_get_cstr(folder_path)); | ||||
|                         break; | ||||
|                     } | ||||
| 
 | ||||
|                     storage_simply_remove(update_task->storage, furi_string_get_cstr(folder_path)); | ||||
|                 } while(false); | ||||
| 
 | ||||
|                 storage_file_free(folder_file); | ||||
|                 furi_string_free(folder_path); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| Before Width: | Height: | Size: 1.7 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 1.7 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 1.7 KiB | 
| Before Width: | Height: | Size: 1.7 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 1.7 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| @ -1,23 +0,0 @@ | ||||
| Filetype: Flipper Animation | ||||
| Version: 1 | ||||
| 
 | ||||
| Width: 128 | ||||
| Height: 64 | ||||
| Passive frames: 10 | ||||
| Active frames: 18 | ||||
| Frames order: 0 1 2 1 0 1 2 1 0 1 2 3 4 5 6 5 4 7 2 8 9 10 11 10 9 10 11 12 | ||||
| Active cycles: 1 | ||||
| Frame rate: 2 | ||||
| Duration: 3600 | ||||
| Active cooldown: 7 | ||||
| 
 | ||||
| Bubble slots: 1 | ||||
| 
 | ||||
| Slot: 0 | ||||
| X: 11 | ||||
| Y: 19 | ||||
| Text:  HAPPY\nHOLIDAYS! | ||||
| AlignH: Right | ||||
| AlignV: Center | ||||
| StartFrame: 22 | ||||
| EndFrame: 27 | ||||
							
								
								
									
										7
									
								
								assets/dolphin/external/manifest.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -36,13 +36,6 @@ Min level: 1 | ||||
| Max level: 1 | ||||
| Weight: 3 | ||||
| 
 | ||||
| Name: L1_Happy_holidays_128x64 | ||||
| Min butthurt: 0 | ||||
| Max butthurt: 14 | ||||
| Min level: 1 | ||||
| Max level: 3 | ||||
| Weight: 4 | ||||
| 
 | ||||
| Name: L1_Read_books_128x64 | ||||
| Min butthurt: 0 | ||||
| Max butthurt: 8 | ||||
|  | ||||
| @ -285,3 +285,46 @@ type: parsed | ||||
| protocol: NECext | ||||
| address: 10 E7 00 00 | ||||
| command: 41 BE 00 00 | ||||
| #  | ||||
| # Model: Grundig CMS 5000 | ||||
| name: Power | ||||
| type: parsed | ||||
| protocol: NECext | ||||
| address: 30 FC 00 00 | ||||
| command: 10 EF 00 00 | ||||
| # Also Pause | ||||
| name: Play | ||||
| type: parsed | ||||
| protocol: NECext | ||||
| address: 30 FC 00 00 | ||||
| command: 02 FD 00 00 | ||||
| # | ||||
| name: Vol_up | ||||
| type: parsed | ||||
| protocol: NECext | ||||
| address: 30 FC 00 00 | ||||
| command: 0D F2 00 00 | ||||
| #  | ||||
| name: Vol_dn | ||||
| type: parsed | ||||
| protocol: NECext | ||||
| address: 30 FC 00 00 | ||||
| command: 17 E8 00 00 | ||||
| # | ||||
| name: Next | ||||
| type: parsed | ||||
| protocol: NECext | ||||
| address: 30 FC 00 00 | ||||
| command: 13 EC 00 00 | ||||
| #  | ||||
| name: Prev | ||||
| type: parsed | ||||
| protocol: NECext | ||||
| address: 30 FC 00 00 | ||||
| command: 11 EE 00 00 | ||||
| #  | ||||
| name: Mute | ||||
| type: parsed | ||||
| protocol: NECext | ||||
| address: 30 FC 00 00 | ||||
| command: 0C F3 00 00 | ||||
|  | ||||
| Before Width: | Height: | Size: 562 B After Width: | Height: | Size: 656 B | 
| @ -3,17 +3,27 @@ | ||||
| FBT is the entry point for firmware-related commands and utilities. | ||||
| It is invoked by `./fbt` in the firmware project root directory. Internally, it is a wrapper around [scons](https://scons.org/) build system. | ||||
| 
 | ||||
| ## Requirements | ||||
| ## Environment | ||||
| 
 | ||||
| Install Python packages required by assets build scripts: `pip3 install -r scripts/requirements.txt` | ||||
| To use `fbt`, you only need `git` installed in your system. | ||||
| 
 | ||||
| ## NB | ||||
| `fbt` by default downloads and unpacks a pre-built toolchain, and then modifies environment variables for itself to use it. It does not contaminate your global system's path with the toolchain. | ||||
|  > However, if you wish to use tools supplied with the toolchain outside `fbt`, you can open an *fbt shell*, with properly configured environment. | ||||
|  >    - On Windows, simply run `scripts/toochain/fbtenv.cmd`. | ||||
|  >    - On Linux & MacOS, run `source scripts/toochain/fbtenv.sh` in a new shell. | ||||
|   | ||||
| - `fbt` constructs all referenced environments and their targets' dependency trees on startup. So, to keep startup time as low as possible, we're hiding the construction of certain targets behind command-line options. | ||||
| - `fbt` always performs `git submodule update --init` on start, unless you set `FBT_NO_SYNC=1` in the environment: | ||||
|  If your system is not supported by pre-built toolchain variants or you want to use custom versions of dependencies, you can `set FBT_NOENV=1`. `fbt` will skip toolchain & environment configuration and will expect all tools to be available on your system's `PATH`. *(this option is not available on Windows)* | ||||
|   | ||||
|  If `FBT_TOOLCHAIN_PATH` variable is set, `fbt` will use that directory to unpack toolchain into. By default, it downloads toolchain into `toolchain` subdirectory repo's root. | ||||
| 
 | ||||
| If you want to enable extra debug output for `fbt` and toolchain management scripts, you can `set FBT_VERBOSE=1`. | ||||
| 
 | ||||
| `fbt` always performs `git submodule update --init` on start, unless you set `FBT_NO_SYNC=1` in the environment: | ||||
|   - On Windows, it's `set "FBT_NO_SYNC=1"` in the shell you're running `fbt` from | ||||
|   - On \*nix, it's `$ FBT_NO_SYNC=1 ./fbt ...` | ||||
| - `fbt` builds updater & firmware in separate subdirectories in `build`, and their names depend on optimization settings (`COMPACT` & `DEBUG` options). However, for ease of integration with IDEs, the latest built variant's directory is always linked as `built/latest`. Additionally, `compile_commands.json` is generated in that folder (used for code completion support in IDE). | ||||
| 
 | ||||
|  > There are more variables controlling basic `fbt` behavior. See `fbt` & `fbtenv` scripts' sources for details. | ||||
| 
 | ||||
| 
 | ||||
| ## Invoking FBT | ||||
| 
 | ||||
| @ -23,6 +33,12 @@ To build with FBT, call it and specify configuration options & targets to build. | ||||
| 
 | ||||
| To run cleanup (think of `make clean`) for specified targets, add the `-c` option. | ||||
| 
 | ||||
| ## Build directories | ||||
| 
 | ||||
| `fbt` builds updater & firmware in separate subdirectories in `build`, and their names depend on optimization settings (`COMPACT` & `DEBUG` options). However, for ease of integration with IDEs, the latest built variant's directory is always linked as `built/latest`. Additionally, `compile_commands.json` is generated in that folder (it is used for code completion support in IDEs). | ||||
|   | ||||
| `build/latest` symlink & compilation database are only updated upon *firmware build targets* - that is, when you're re-building the firmware itself. Running other tasks, like firmware flashing or building update bundles *for a different debug/release configuration or hardware target*, does not update `built/latest` dir to point to that configuration. | ||||
| 
 | ||||
| ## VSCode integration | ||||
| 
 | ||||
| `fbt` includes basic development environment configuration for VS Code. Run `./fbt vscode_dist` to deploy it. That will copy the initial environment configuration to the `.vscode` folder. After that, you can use that configuration by starting VS Code and choosing the firmware root folder in the "File > Open Folder" menu. | ||||
|  | ||||
| @ -75,6 +75,14 @@ Can be combined with a special key command or a single character. | ||||
| | ------- | ----------- | ----------------- | | ||||
| | STRING  | Text string | Print text string | | ||||
| 
 | ||||
| ## String delay | ||||
| 
 | ||||
| Delay between keypresses. | ||||
| |Command|Parameters|Notes| | ||||
| |-|-|-| | ||||
| |STRING_DELAY|Delay value in ms|Applied once to next appearing string| | ||||
| |STRINGDELAY|Delay value in ms|Same as STRING_DELAY| | ||||
| 
 | ||||
| ## Repeat | ||||
| 
 | ||||
| | Command | Parameters                   | Notes                   | | ||||
|  | ||||
							
								
								
									
										11
									
								
								fbt
									
									
									
									
									
								
							
							
						
						| @ -6,16 +6,21 @@ set -eu; | ||||
| 
 | ||||
| # private variables | ||||
| SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd -P)"; | ||||
| SCONS_DEFAULT_FLAGS="-Q --warn=target-not-built"; | ||||
| SCONS_EP="python3 -m SCons" | ||||
| SCONS_DEFAULT_FLAGS="--warn=target-not-built"; | ||||
| SCONS_EP="python3 -m SCons"; | ||||
| 
 | ||||
| # public variables | ||||
| FBT_NOENV="${FBT_NOENV:-""}"; | ||||
| FBT_NO_SYNC="${FBT_NO_SYNC:-""}"; | ||||
| FBT_TOOLCHAIN_PATH="${FBT_TOOLCHAIN_PATH:-$SCRIPT_PATH}"; | ||||
| FBT_VERBOSE="${FBT_VERBOSE:-""}"; | ||||
| 
 | ||||
| if [ -z "$FBT_NOENV" ]; then | ||||
|     . "$SCRIPT_PATH/scripts/toolchain/fbtenv.sh"; | ||||
|     FBT_VERBOSE="$FBT_VERBOSE" . "$SCRIPT_PATH/scripts/toolchain/fbtenv.sh"; | ||||
| fi | ||||
| 
 | ||||
| if [ -z "$FBT_VERBOSE" ]; then | ||||
|     SCONS_DEFAULT_FLAGS="$SCONS_DEFAULT_FLAGS -Q"; | ||||
| fi | ||||
| 
 | ||||
| if [ -z "$FBT_NO_SYNC" ]; then | ||||
|  | ||||
							
								
								
									
										7
									
								
								fbt.cmd
									
									
									
									
									
								
							
							
						
						| @ -12,5 +12,10 @@ if [%FBT_NO_SYNC%] == [] ( | ||||
|     ) | ||||
| ) | ||||
| 
 | ||||
| set "SCONS_DEFAULT_FLAGS=-Q --warn=target-not-built" | ||||
| set "SCONS_DEFAULT_FLAGS=--warn=target-not-built" | ||||
| 
 | ||||
| if not defined FBT_VERBOSE ( | ||||
|     set "SCONS_DEFAULT_FLAGS=%SCONS_DEFAULT_FLAGS% -Q" | ||||
| ) | ||||
| 
 | ||||
| %SCONS_EP% %SCONS_DEFAULT_FLAGS% %* | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| entry,status,name,type,params | ||||
| Version,+,14.0,, | ||||
| Version,+,15.0,, | ||||
| Header,+,applications/services/bt/bt_service/bt.h,, | ||||
| Header,+,applications/services/cli/cli.h,, | ||||
| Header,+,applications/services/cli/cli_vcp.h,, | ||||
| @ -979,7 +979,7 @@ Function,+,furi_hal_power_enable_external_3_3v,void, | ||||
| Function,+,furi_hal_power_enable_otg,void, | ||||
| Function,+,furi_hal_power_gauge_is_ok,_Bool, | ||||
| Function,+,furi_hal_power_get_bat_health_pct,uint8_t, | ||||
| Function,+,furi_hal_power_get_battery_charging_voltage,float, | ||||
| Function,+,furi_hal_power_get_battery_charge_voltage_limit,float, | ||||
| Function,+,furi_hal_power_get_battery_current,float,FuriHalPowerIC | ||||
| Function,+,furi_hal_power_get_battery_design_capacity,uint32_t, | ||||
| Function,+,furi_hal_power_get_battery_full_capacity,uint32_t, | ||||
| @ -998,7 +998,7 @@ Function,+,furi_hal_power_is_charging_done,_Bool, | ||||
| Function,+,furi_hal_power_is_otg_enabled,_Bool, | ||||
| Function,+,furi_hal_power_off,void, | ||||
| Function,+,furi_hal_power_reset,void, | ||||
| Function,+,furi_hal_power_set_battery_charging_voltage,void,float | ||||
| Function,+,furi_hal_power_set_battery_charge_voltage_limit,void,float | ||||
| Function,+,furi_hal_power_shutdown,void, | ||||
| Function,+,furi_hal_power_sleep,void, | ||||
| Function,+,furi_hal_power_sleep_available,_Bool, | ||||
|  | ||||
| 
 | 
| @ -1,5 +1,5 @@ | ||||
| entry,status,name,type,params | ||||
| Version,+,14.0,, | ||||
| Version,+,15.0,, | ||||
| Header,+,applications/services/bt/bt_service/bt.h,, | ||||
| Header,+,applications/services/cli/cli.h,, | ||||
| Header,+,applications/services/cli/cli_vcp.h,, | ||||
| @ -1171,6 +1171,7 @@ Function,+,furi_hal_mpu_protect_disable,void,FuriHalMpuRegion | ||||
| Function,+,furi_hal_mpu_protect_no_access,void,"FuriHalMpuRegion, uint32_t, FuriHalMPURegionSize" | ||||
| Function,+,furi_hal_mpu_protect_read_only,void,"FuriHalMpuRegion, uint32_t, FuriHalMPURegionSize" | ||||
| Function,+,furi_hal_nfc_activate_nfca,_Bool,"uint32_t, uint32_t*" | ||||
| Function,-,furi_hal_nfc_deinit,void, | ||||
| Function,+,furi_hal_nfc_detect,_Bool,"FuriHalNfcDevData*, uint32_t" | ||||
| Function,+,furi_hal_nfc_emulate_nfca,_Bool,"uint8_t*, uint8_t, uint8_t*, uint8_t, FuriHalNfcEmulateCallback, void*, uint32_t" | ||||
| Function,+,furi_hal_nfc_exit_sleep,void, | ||||
| @ -1210,7 +1211,7 @@ Function,+,furi_hal_power_enable_external_3_3v,void, | ||||
| Function,+,furi_hal_power_enable_otg,void, | ||||
| Function,+,furi_hal_power_gauge_is_ok,_Bool, | ||||
| Function,+,furi_hal_power_get_bat_health_pct,uint8_t, | ||||
| Function,+,furi_hal_power_get_battery_charging_voltage,float, | ||||
| Function,+,furi_hal_power_get_battery_charge_voltage_limit,float, | ||||
| Function,+,furi_hal_power_get_battery_current,float,FuriHalPowerIC | ||||
| Function,+,furi_hal_power_get_battery_design_capacity,uint32_t, | ||||
| Function,+,furi_hal_power_get_battery_full_capacity,uint32_t, | ||||
| @ -1229,7 +1230,7 @@ Function,+,furi_hal_power_is_charging_done,_Bool, | ||||
| Function,+,furi_hal_power_is_otg_enabled,_Bool, | ||||
| Function,+,furi_hal_power_off,void, | ||||
| Function,+,furi_hal_power_reset,void, | ||||
| Function,+,furi_hal_power_set_battery_charging_voltage,void,float | ||||
| Function,+,furi_hal_power_set_battery_charge_voltage_limit,void,float | ||||
| Function,+,furi_hal_power_shutdown,void, | ||||
| Function,+,furi_hal_power_sleep,void, | ||||
| Function,+,furi_hal_power_sleep_available,_Bool, | ||||
|  | ||||
| 
 | 
| @ -17,7 +17,6 @@ | ||||
| #define SD_DUMMY_BYTE 0xFF | ||||
| #define SD_ANSWER_RETRY_COUNT 8 | ||||
| #define SD_IDLE_RETRY_COUNT 100 | ||||
| #define SD_BLOCK_SIZE 512 | ||||
| 
 | ||||
| #define FLAG_SET(x, y) (((x) & (y)) == (y)) | ||||
| 
 | ||||
| @ -598,23 +597,6 @@ static SdSpiStatus sd_spi_get_cid(SD_CID* Cid) { | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| static inline bool sd_cache_get(uint32_t address, uint32_t* data) { | ||||
|     uint8_t* cached_data = sector_cache_get(address); | ||||
|     if(cached_data) { | ||||
|         memcpy(data, cached_data, SD_BLOCK_SIZE); | ||||
|         return true; | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| static inline void sd_cache_put(uint32_t address, uint32_t* data) { | ||||
|     sector_cache_put(address, (uint8_t*)data); | ||||
| } | ||||
| 
 | ||||
| static inline void sd_cache_invalidate_range(uint32_t start_sector, uint32_t end_sector) { | ||||
|     sector_cache_invalidate_range(start_sector, end_sector); | ||||
| } | ||||
| 
 | ||||
| static SdSpiStatus | ||||
|     sd_spi_cmd_read_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms) { | ||||
|     uint32_t block_address = address; | ||||
| @ -833,30 +815,12 @@ SdSpiStatus sd_get_card_info(SD_CardInfo* card_info) { | ||||
| 
 | ||||
| SdSpiStatus | ||||
|     sd_read_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms) { | ||||
|     SdSpiStatus status = SdSpiStatusError; | ||||
| 
 | ||||
|     bool single_sector_read = (blocks == 1); | ||||
| 
 | ||||
|     if(single_sector_read) { | ||||
|         if(sd_cache_get(address, data)) { | ||||
|             return SdSpiStatusOK; | ||||
|         } | ||||
| 
 | ||||
|         status = sd_spi_cmd_read_blocks(data, address, blocks, timeout_ms); | ||||
| 
 | ||||
|         if(status == SdSpiStatusOK) { | ||||
|             sd_cache_put(address, data); | ||||
|         } | ||||
|     } else { | ||||
|         status = sd_spi_cmd_read_blocks(data, address, blocks, timeout_ms); | ||||
|     } | ||||
| 
 | ||||
|     SdSpiStatus status = sd_spi_cmd_read_blocks(data, address, blocks, timeout_ms); | ||||
|     return status; | ||||
| } | ||||
| 
 | ||||
| SdSpiStatus | ||||
|     sd_write_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms) { | ||||
|     sd_cache_invalidate_range(address, address + blocks); | ||||
|     SdSpiStatus status = sd_spi_cmd_write_blocks(data, address, blocks, timeout_ms); | ||||
|     return status; | ||||
| } | ||||
|  | ||||
| @ -5,6 +5,7 @@ | ||||
| #define __IO volatile | ||||
| 
 | ||||
| #define SD_TIMEOUT_MS (1000) | ||||
| #define SD_BLOCK_SIZE 512 | ||||
| 
 | ||||
| typedef enum { | ||||
|     SdSpiStatusOK, | ||||
|  | ||||
| @ -36,6 +36,7 @@ | ||||
| /* Includes ------------------------------------------------------------------*/ | ||||
| #include "user_diskio.h" | ||||
| #include <furi_hal.h> | ||||
| #include "sector_cache.h" | ||||
| /* Private typedef -----------------------------------------------------------*/ | ||||
| /* Private define ------------------------------------------------------------*/ | ||||
| 
 | ||||
| @ -79,6 +80,26 @@ Diskio_drvTypeDef USER_Driver = { | ||||
| }; | ||||
| 
 | ||||
| /* Private functions ---------------------------------------------------------*/ | ||||
| static inline bool sd_cache_get(uint32_t address, uint32_t* data) { | ||||
|     uint8_t* cached_data = sector_cache_get(address); | ||||
|     if(cached_data) { | ||||
|         memcpy(data, cached_data, SD_BLOCK_SIZE); | ||||
|         return true; | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| static inline void sd_cache_put(uint32_t address, uint32_t* data) { | ||||
|     sector_cache_put(address, (uint8_t*)data); | ||||
| } | ||||
| 
 | ||||
| static inline void sd_cache_invalidate_range(uint32_t start_sector, uint32_t end_sector) { | ||||
|     sector_cache_invalidate_range(start_sector, end_sector); | ||||
| } | ||||
| 
 | ||||
| static inline void sd_cache_invalidate_all() { | ||||
|     sector_cache_init(); | ||||
| } | ||||
| 
 | ||||
| /**
 | ||||
|   * @brief  Initializes a Drive | ||||
| @ -125,6 +146,14 @@ DRESULT USER_read(BYTE pdrv, BYTE* buff, DWORD sector, UINT count) { | ||||
|     UNUSED(pdrv); | ||||
|     DRESULT res = RES_ERROR; | ||||
| 
 | ||||
|     bool single_sector = count == 1; | ||||
| 
 | ||||
|     if(single_sector) { | ||||
|         if(sd_cache_get(sector, (uint32_t*)buff)) { | ||||
|             return RES_OK; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     furi_hal_spi_acquire(&furi_hal_spi_bus_handle_sd_fast); | ||||
|     furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_sd_fast; | ||||
| 
 | ||||
| @ -145,6 +174,10 @@ DRESULT USER_read(BYTE pdrv, BYTE* buff, DWORD sector, UINT count) { | ||||
|     furi_hal_sd_spi_handle = NULL; | ||||
|     furi_hal_spi_release(&furi_hal_spi_bus_handle_sd_fast); | ||||
| 
 | ||||
|     if(single_sector && res == RES_OK) { | ||||
|         sd_cache_put(sector, (uint32_t*)buff); | ||||
|     } | ||||
| 
 | ||||
|     return res; | ||||
|     /* USER CODE END READ */ | ||||
| } | ||||
| @ -164,6 +197,8 @@ DRESULT USER_write(BYTE pdrv, const BYTE* buff, DWORD sector, UINT count) { | ||||
|     UNUSED(pdrv); | ||||
|     DRESULT res = RES_ERROR; | ||||
| 
 | ||||
|     sd_cache_invalidate_range(sector, sector + count); | ||||
| 
 | ||||
|     furi_hal_spi_acquire(&furi_hal_spi_bus_handle_sd_fast); | ||||
|     furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_sd_fast; | ||||
| 
 | ||||
| @ -175,6 +210,8 @@ DRESULT USER_write(BYTE pdrv, const BYTE* buff, DWORD sector, UINT count) { | ||||
|         res = RES_OK; | ||||
|         while(sd_get_card_state() != SdSpiStatusOK) { | ||||
|             if(furi_hal_cortex_timer_is_expired(timer)) { | ||||
|                 sd_cache_invalidate_all(); | ||||
| 
 | ||||
|                 res = RES_ERROR; | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
| @ -24,13 +24,29 @@ FuriEventFlag* event = NULL; | ||||
| #define FURI_HAL_NFC_UID_INCOMPLETE (0x04) | ||||
| 
 | ||||
| void furi_hal_nfc_init() { | ||||
|     furi_assert(!event); | ||||
|     event = furi_event_flag_alloc(); | ||||
| 
 | ||||
|     ReturnCode ret = rfalNfcInitialize(); | ||||
|     if(ret == ERR_NONE) { | ||||
|         furi_hal_nfc_start_sleep(); | ||||
|         event = furi_event_flag_alloc(); | ||||
|         FURI_LOG_I(TAG, "Init OK"); | ||||
|     } else { | ||||
|         FURI_LOG_W(TAG, "Initialization failed, RFAL returned: %d", ret); | ||||
|         FURI_LOG_W(TAG, "Init Failed, RFAL returned: %d", ret); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void furi_hal_nfc_deinit() { | ||||
|     ReturnCode ret = rfalDeinitialize(); | ||||
|     if(ret == ERR_NONE) { | ||||
|         FURI_LOG_I(TAG, "Deinit OK"); | ||||
|     } else { | ||||
|         FURI_LOG_W(TAG, "Deinit Failed, RFAL returned: %d", ret); | ||||
|     } | ||||
| 
 | ||||
|     if(event) { | ||||
|         furi_event_flag_free(event); | ||||
|         event = NULL; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -101,6 +101,10 @@ typedef struct { | ||||
|  */ | ||||
| void furi_hal_nfc_init(); | ||||
| 
 | ||||
| /** Deinit nfc
 | ||||
|  */ | ||||
| void furi_hal_nfc_deinit(); | ||||
| 
 | ||||
| /** Check if nfc worker is busy
 | ||||
|  * | ||||
|  * @return     true if busy | ||||
|  | ||||
| @ -341,14 +341,14 @@ bool furi_hal_power_is_otg_enabled() { | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| float furi_hal_power_get_battery_charging_voltage() { | ||||
| float furi_hal_power_get_battery_charge_voltage_limit() { | ||||
|     furi_hal_i2c_acquire(&furi_hal_i2c_handle_power); | ||||
|     float ret = (float)bq25896_get_vreg_voltage(&furi_hal_i2c_handle_power) / 1000.0f; | ||||
|     furi_hal_i2c_release(&furi_hal_i2c_handle_power); | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| void furi_hal_power_set_battery_charging_voltage(float voltage) { | ||||
| void furi_hal_power_set_battery_charge_voltage_limit(float voltage) { | ||||
|     furi_hal_i2c_acquire(&furi_hal_i2c_handle_power); | ||||
|     // Adding 0.0005 is necessary because 4.016f is 4.015999794000, which gets truncated
 | ||||
|     bq25896_set_vreg_voltage(&furi_hal_i2c_handle_power, (uint16_t)(voltage * 1000.0f + 0.0005f)); | ||||
| @ -486,7 +486,7 @@ void furi_hal_power_info_get(PropertyValueCallback out, char sep, void* context) | ||||
|         property_value_out(&property_context, NULL, 2, "format", "major", "2"); | ||||
|         property_value_out(&property_context, NULL, 2, "format", "minor", "1"); | ||||
|     } else { | ||||
|         property_value_out(&property_context, NULL, 3, "power", "info", "major", "1"); | ||||
|         property_value_out(&property_context, NULL, 3, "power", "info", "major", "2"); | ||||
|         property_value_out(&property_context, NULL, 3, "power", "info", "minor", "1"); | ||||
|     } | ||||
| 
 | ||||
| @ -505,8 +505,10 @@ void furi_hal_power_info_get(PropertyValueCallback out, char sep, void* context) | ||||
|     } | ||||
| 
 | ||||
|     property_value_out(&property_context, NULL, 2, "charge", "state", charge_state); | ||||
|     uint16_t charge_voltage = (uint16_t)(furi_hal_power_get_battery_charging_voltage() * 1000.f); | ||||
|     property_value_out(&property_context, "%u", 2, "charge", "voltage", charge_voltage); | ||||
|     uint16_t charge_voltage_limit = | ||||
|         (uint16_t)(furi_hal_power_get_battery_charge_voltage_limit() * 1000.f); | ||||
|     property_value_out( | ||||
|         &property_context, "%u", 3, "charge", "voltage", "limit", charge_voltage_limit); | ||||
|     uint16_t voltage = | ||||
|         (uint16_t)(furi_hal_power_get_battery_voltage(FuriHalPowerICFuelGauge) * 1000.f); | ||||
|     property_value_out(&property_context, "%u", 2, "battery", "voltage", voltage); | ||||
|  | ||||
| @ -121,21 +121,21 @@ void furi_hal_power_check_otg_status(); | ||||
|  */ | ||||
| bool furi_hal_power_is_otg_enabled(); | ||||
| 
 | ||||
| /** Get battery charging voltage in V
 | ||||
| /** Get battery charge voltage limit in V
 | ||||
|  * | ||||
|  * @return     voltage in V | ||||
|  */ | ||||
| float furi_hal_power_get_battery_charging_voltage(); | ||||
| float furi_hal_power_get_battery_charge_voltage_limit(); | ||||
| 
 | ||||
| /** Set battery charging voltage in V
 | ||||
| /** Set battery charge voltage limit in V
 | ||||
|  * | ||||
|  * Invalid values will be clamped to the nearest valid value. | ||||
|  * Invalid values will be clamped downward to the nearest valid value. | ||||
|  * | ||||
|  * @param      voltage[in]  voltage in V | ||||
|  * | ||||
|  * @return     voltage in V | ||||
|  */ | ||||
| void furi_hal_power_set_battery_charging_voltage(float voltage); | ||||
| void furi_hal_power_set_battery_charge_voltage_limit(float voltage); | ||||
| 
 | ||||
| /** Get remaining battery battery capacity in mAh
 | ||||
|  * | ||||
|  | ||||
| @ -96,9 +96,9 @@ static void furi_thread_body(void* context) { | ||||
|     furi_assert(thread->state == FuriThreadStateRunning); | ||||
| 
 | ||||
|     if(thread->is_service) { | ||||
|         FURI_LOG_E( | ||||
|         FURI_LOG_W( | ||||
|             TAG, | ||||
|             "%s service thread exited. Thread memory cannot be reclaimed.", | ||||
|             "%s service thread TCB memory will not be reclaimed", | ||||
|             thread->name ? thread->name : "<unknown service>"); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
| #include <furi.h> | ||||
| #include <furi_hal_version.h> | ||||
| #include <furi_hal_memory.h> | ||||
| #include <furi_hal_rtc.h> | ||||
| 
 | ||||
| #define TAG "Flipper" | ||||
| 
 | ||||
| @ -29,10 +30,10 @@ static void flipper_print_version(const char* target, const Version* version) { | ||||
| void flipper_init() { | ||||
|     flipper_print_version("Firmware", furi_hal_version_get_firmware_version()); | ||||
| 
 | ||||
|     FURI_LOG_I(TAG, "starting services"); | ||||
|     FURI_LOG_I(TAG, "Boot mode %d, starting services", furi_hal_rtc_get_boot_mode()); | ||||
| 
 | ||||
|     for(size_t i = 0; i < FLIPPER_SERVICES_COUNT; i++) { | ||||
|         FURI_LOG_I(TAG, "starting service %s", FLIPPER_SERVICES[i].name); | ||||
|         FURI_LOG_I(TAG, "Starting service %s", FLIPPER_SERVICES[i].name); | ||||
| 
 | ||||
|         FuriThread* thread = furi_thread_alloc_ex( | ||||
|             FLIPPER_SERVICES[i].name, | ||||
| @ -44,7 +45,7 @@ void flipper_init() { | ||||
|         furi_thread_start(thread); | ||||
|     } | ||||
| 
 | ||||
|     FURI_LOG_I(TAG, "services startup complete"); | ||||
|     FURI_LOG_I(TAG, "Startup complete"); | ||||
| } | ||||
| 
 | ||||
| void vApplicationGetIdleTaskMemory( | ||||
|  | ||||
| @ -140,19 +140,16 @@ uint16_t bq25896_get_vreg_voltage(FuriHalI2cBusHandle* handle) { | ||||
| 
 | ||||
| void bq25896_set_vreg_voltage(FuriHalI2cBusHandle* handle, uint16_t vreg_voltage) { | ||||
|     if(vreg_voltage < 3840) { | ||||
|         // Minimum value is 3840 mV
 | ||||
|         bq25896_regs.r06.VREG = 0; | ||||
|     } else { | ||||
|         // Find the nearest voltage value (subtract offset, divide into sections)
 | ||||
|         // Values are truncated downward as needed (e.g. 4200mV -> 4192 mV)
 | ||||
|         bq25896_regs.r06.VREG = (uint8_t)((vreg_voltage - 3840) / 16); | ||||
|         // Minimum valid value is 3840 mV
 | ||||
|         vreg_voltage = 3840; | ||||
|     } else if(vreg_voltage > 4208) { | ||||
|         // Maximum safe value is 4208 mV
 | ||||
|         vreg_voltage = 4208; | ||||
|     } | ||||
| 
 | ||||
|     // Do not allow values above 23 (0x17, 4208mV)
 | ||||
|     // Exceeding 4.2v will overcharge the battery!
 | ||||
|     if(bq25896_regs.r06.VREG > 23) { | ||||
|         bq25896_regs.r06.VREG = 23; | ||||
|     } | ||||
|     // Find the nearest voltage value (subtract offset, divide into sections)
 | ||||
|     // Values are truncated downward as needed (e.g. 4200mV -> 4192 mV)
 | ||||
|     bq25896_regs.r06.VREG = (uint8_t)((vreg_voltage - 3840) / 16); | ||||
| 
 | ||||
|     // Apply changes
 | ||||
|     furi_hal_i2c_write_reg_8( | ||||
|  | ||||
| @ -36,10 +36,10 @@ void bq25896_disable_otg(FuriHalI2cBusHandle* handle); | ||||
| /** Is otg enabled */ | ||||
| bool bq25896_is_otg_enabled(FuriHalI2cBusHandle* handle); | ||||
| 
 | ||||
| /** Get VREG (charging) voltage in mV */ | ||||
| /** Get VREG (charging limit) voltage in mV */ | ||||
| uint16_t bq25896_get_vreg_voltage(FuriHalI2cBusHandle* handle); | ||||
| 
 | ||||
| /** Set VREG (charging) voltage in mV
 | ||||
| /** Set VREG (charging limit) voltage in mV
 | ||||
|  * | ||||
|  * Valid range: 3840mV - 4208mV, in steps of 16mV | ||||
|  */ | ||||
|  | ||||
| @ -56,6 +56,8 @@ void nfc_worker_start( | ||||
|     while(furi_hal_nfc_is_busy()) { | ||||
|         furi_delay_ms(10); | ||||
|     } | ||||
|     furi_hal_nfc_deinit(); | ||||
|     furi_hal_nfc_init(); | ||||
| 
 | ||||
|     nfc_worker->callback = callback; | ||||
|     nfc_worker->context = context; | ||||
|  | ||||
| @ -22,7 +22,6 @@ def _convert_image(source_filename: str): | ||||
| 
 | ||||
| 
 | ||||
| class DolphinBubbleAnimation: | ||||
| 
 | ||||
|     FILE_TYPE = "Flipper Animation" | ||||
|     FILE_VERSION = 1 | ||||
| 
 | ||||
| @ -243,7 +242,6 @@ class DolphinBubbleAnimation: | ||||
| 
 | ||||
| 
 | ||||
| class DolphinManifest: | ||||
| 
 | ||||
|     FILE_TYPE = "Flipper Animation Manifest" | ||||
|     FILE_VERSION = 1 | ||||
| 
 | ||||
|  | ||||
| @ -105,7 +105,7 @@ def file2image(file): | ||||
|     data_enc = bytearray([len(data_enc) & 0xFF, len(data_enc) >> 8]) + data_enc | ||||
| 
 | ||||
|     # Use encoded data only if its length less than original, including header | ||||
|     if len(data_enc) < len(data_bin) + 1: | ||||
|     if len(data_enc) + 2 < len(data_bin) + 1: | ||||
|         data = b"\x01\x00" + data_enc | ||||
|     else: | ||||
|         data = b"\x00" + data_bin | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import serial.tools.list_ports as list_ports | ||||
| 
 | ||||
| 
 | ||||
| # Returns a valid port or None, if it cannot be found | ||||
| def resolve_port(logger, portname: str = "auto"): | ||||
|     if portname != "auto": | ||||
|  | ||||
| @ -173,12 +173,14 @@ class Templite: | ||||
|         """Renders the template according to the given namespace.""" | ||||
|         stack = [] | ||||
|         namespace["__file__"] = self.file | ||||
| 
 | ||||
|         # add write method | ||||
|         def write(*args): | ||||
|             for value in args: | ||||
|                 stack.append(str(value)) | ||||
| 
 | ||||
|         namespace["write"] = write | ||||
| 
 | ||||
|         # add include method | ||||
|         def include(file): | ||||
|             if not os.path.isabs(file): | ||||
|  | ||||
| @ -13,7 +13,7 @@ if not ["%FBT_NOENV%"] == [""] ( | ||||
|     exit /b 0 | ||||
| ) | ||||
| 
 | ||||
| set "FLIPPER_TOOLCHAIN_VERSION=19" | ||||
| set "FLIPPER_TOOLCHAIN_VERSION=21" | ||||
| 
 | ||||
| if ["%FBT_TOOLCHAIN_ROOT%"] == [""] ( | ||||
|     set "FBT_TOOLCHAIN_ROOT=%FBT_ROOT%\toolchain\x86_64-windows" | ||||
| @ -31,7 +31,13 @@ if not exist "%FBT_TOOLCHAIN_VERSION_FILE%" ( | ||||
| 
 | ||||
| set /p REAL_TOOLCHAIN_VERSION=<"%FBT_TOOLCHAIN_VERSION_FILE%" | ||||
| if not "%REAL_TOOLCHAIN_VERSION%" == "%FLIPPER_TOOLCHAIN_VERSION%" ( | ||||
|     echo FBT: starting toolchain upgrade process.. | ||||
|     powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" %flipper_toolchain_version% "%FBT_TOOLCHAIN_ROOT%" | ||||
|     set /p REAL_TOOLCHAIN_VERSION=<"%FBT_TOOLCHAIN_VERSION_FILE%" | ||||
| ) | ||||
| 
 | ||||
| if defined FBT_VERBOSE ( | ||||
|     echo FBT: using toolchain version %REAL_TOOLCHAIN_VERSION% | ||||
| ) | ||||
| 
 | ||||
| set "HOME=%USERPROFILE%" | ||||
|  | ||||
| @ -5,15 +5,16 @@ | ||||
| # public variables | ||||
| DEFAULT_SCRIPT_PATH="$(pwd -P)"; | ||||
| SCRIPT_PATH="${SCRIPT_PATH:-$DEFAULT_SCRIPT_PATH}"; | ||||
| FBT_TOOLCHAIN_VERSION="${FBT_TOOLCHAIN_VERSION:-"19"}"; | ||||
| FBT_TOOLCHAIN_VERSION="${FBT_TOOLCHAIN_VERSION:-"21"}"; | ||||
| FBT_TOOLCHAIN_PATH="${FBT_TOOLCHAIN_PATH:-$SCRIPT_PATH}"; | ||||
| FBT_VERBOSE="${FBT_VERBOSE:-""}"; | ||||
| 
 | ||||
| fbtenv_show_usage() | ||||
| { | ||||
|     echo "Running this script manually is wrong, please source it"; | ||||
|     echo "Example:"; | ||||
|     printf "\tsource scripts/toolchain/fbtenv.sh\n"; | ||||
|     echo "To restore your environment source fbtenv.sh with '--restore'." | ||||
|     echo "To restore your environment, source fbtenv.sh with '--restore'." | ||||
|     echo "Example:"; | ||||
|     printf "\tsource scripts/toolchain/fbtenv.sh --restore\n"; | ||||
| } | ||||
| @ -35,16 +36,26 @@ fbtenv_restore_env() | ||||
|     PATH="$(echo "$PATH" | /usr/bin/sed "s/$TOOLCHAIN_ARCH_DIR_SED\/bin://g")"; | ||||
|     PATH="$(echo "$PATH" | /usr/bin/sed "s/$TOOLCHAIN_ARCH_DIR_SED\/protobuf\/bin://g")"; | ||||
|     PATH="$(echo "$PATH" | /usr/bin/sed "s/$TOOLCHAIN_ARCH_DIR_SED\/openocd\/bin://g")"; | ||||
|     PATH="$(echo "$PATH" | /usr/bin/sed "s/$TOOLCHAIN_ARCH_DIR_SED\/openssl\/bin://g")"; | ||||
|     if [ -n "${PS1:-""}" ]; then | ||||
|         PS1="$(echo "$PS1" | sed 's/\[fbt\]//g')"; | ||||
|     elif [ -n "${PROMPT:-""}" ]; then | ||||
|         PROMPT="$(echo "$PROMPT" | sed 's/\[fbt\]//g')"; | ||||
|     fi | ||||
| 
 | ||||
|     PYTHONNOUSERSITE="$SAVED_PYTHONNOUSERSITE"; | ||||
|     PYTHONPATH="$SAVED_PYTHONPATH"; | ||||
|     PYTHONHOME="$SAVED_PYTHONHOME"; | ||||
|     if [ -n "$SAVED_SSL_CERT_FILE" ]; then | ||||
|         export SSL_CERT_FILE="$SAVED_SSL_CERT_FILE"; | ||||
|         export REQUESTS_CA_BUNDLE="$SAVED_REQUESTS_CA_BUNDLE"; | ||||
|     else | ||||
|         unset SSL_CERT_FILE; | ||||
|         unset REQUESTS_CA_BUNDLE; | ||||
|     fi | ||||
|     export PYTHONNOUSERSITE="$SAVED_PYTHONNOUSERSITE"; | ||||
|     export PYTHONPATH="$SAVED_PYTHONPATH"; | ||||
|     export PYTHONHOME="$SAVED_PYTHONHOME"; | ||||
| 
 | ||||
|     unset SAVED_SSL_CERT_FILE; | ||||
|     unset SAVED_REQUESTS_CA_BUNDLE; | ||||
|     unset SAVED_PYTHONNOUSERSITE; | ||||
|     unset SAVED_PYTHONPATH; | ||||
|     unset SAVED_PYTHONHOME; | ||||
| @ -121,7 +132,7 @@ fbtenv_get_kernel_type() | ||||
|         TOOLCHAIN_ARCH_DIR="$FBT_TOOLCHAIN_PATH/toolchain/x86_64-linux"; | ||||
|         TOOLCHAIN_URL="https://update.flipperzero.one/builds/toolchain/gcc-arm-none-eabi-10.3-x86_64-linux-flipper-$FBT_TOOLCHAIN_VERSION.tar.gz"; | ||||
|     elif echo "$SYS_TYPE" | grep -q "MINGW"; then | ||||
|         echo "In MinGW shell use \"[u]fbt.cmd\" instead of \"[u]fbt\""; | ||||
|         echo "In MinGW shell, use \"[u]fbt.cmd\" instead of \"[u]fbt\""; | ||||
|         return 1; | ||||
|     else | ||||
|         echo "Your system configuration is not supported. Sorry.. Please report us your configuration."; | ||||
| @ -248,6 +259,7 @@ fbtenv_check_download_toolchain() | ||||
|     elif [ ! -f "$TOOLCHAIN_ARCH_DIR/VERSION" ]; then | ||||
|         fbtenv_download_toolchain || return 1; | ||||
|     elif [ "$(cat "$TOOLCHAIN_ARCH_DIR/VERSION")" -ne "$FBT_TOOLCHAIN_VERSION" ]; then | ||||
|         echo "FBT: starting toolchain upgrade process.." | ||||
|         fbtenv_download_toolchain || return 1; | ||||
|     fi | ||||
|     return 0; | ||||
| @ -269,6 +281,13 @@ fbtenv_download_toolchain() | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| fbtenv_print_version() | ||||
| { | ||||
|     if [ -n "$FBT_VERBOSE" ]; then | ||||
|         echo "FBT: using toolchain version $(cat "$TOOLCHAIN_ARCH_DIR/VERSION")"; | ||||
|     fi | ||||
| } | ||||
| 
 | ||||
| fbtenv_main() | ||||
| { | ||||
|     fbtenv_check_sourced || return 1; | ||||
| @ -281,18 +300,25 @@ fbtenv_main() | ||||
|     fbtenv_check_script_path || return 1; | ||||
|     fbtenv_check_download_toolchain || return 1; | ||||
|     fbtenv_set_shell_prompt; | ||||
|     fbtenv_print_version; | ||||
|     PATH="$TOOLCHAIN_ARCH_DIR/python/bin:$PATH"; | ||||
|     PATH="$TOOLCHAIN_ARCH_DIR/bin:$PATH"; | ||||
|     PATH="$TOOLCHAIN_ARCH_DIR/protobuf/bin:$PATH"; | ||||
|     PATH="$TOOLCHAIN_ARCH_DIR/openocd/bin:$PATH"; | ||||
|     PATH="$TOOLCHAIN_ARCH_DIR/openssl/bin:$PATH"; | ||||
|     export PATH; | ||||
| 
 | ||||
|     SAVED_PYTHONNOUSERSITE="${PYTHONNOUSERSITE:-""}"; | ||||
|     SAVED_PYTHONPATH="${PYTHONPATH:-""}"; | ||||
|     SAVED_PYTHONHOME="${PYTHONHOME:-""}"; | ||||
|     export SAVED_SSL_CERT_FILE="${SSL_CERT_FILE:-""}"; | ||||
|     export SAVED_REQUESTS_CA_BUNDLE="${REQUESTS_CA_BUNDLE:-""}"; | ||||
|     export SAVED_PYTHONNOUSERSITE="${PYTHONNOUSERSITE:-""}"; | ||||
|     export SAVED_PYTHONPATH="${PYTHONPATH:-""}"; | ||||
|     export SAVED_PYTHONHOME="${PYTHONHOME:-""}"; | ||||
| 
 | ||||
|     PYTHONNOUSERSITE=1; | ||||
|     PYTHONPATH=; | ||||
|     PYTHONHOME=; | ||||
|     export SSL_CERT_FILE="$TOOLCHAIN_ARCH_DIR/python/lib/python3.11/site-packages/certifi/cacert.pem"; | ||||
|     export REQUESTS_CA_BUNDLE="$SSL_CERT_FILE"; | ||||
|     export PYTHONNOUSERSITE=1; | ||||
|     export PYTHONPATH=; | ||||
|     export PYTHONHOME=; | ||||
| } | ||||
| 
 | ||||
| fbtenv_main "${1:-""}"; | ||||
|  | ||||
| @ -199,7 +199,7 @@ class Main(App): | ||||
| 
 | ||||
|     def disclaimer(self): | ||||
|         self.logger.error( | ||||
|             "You might brick you device into a state in which you'd need an SWD programmer to fix it." | ||||
|             "You might brick your device into a state in which you'd need an SWD programmer to fix it." | ||||
|         ) | ||||
|         self.logger.error( | ||||
|             "Please confirm that you REALLY want to do that with --I-understand-what-I-am-doing=yes" | ||||
|  | ||||
 Aleksandr Kutuzov
						Aleksandr Kutuzov