20 Commits
Author SHA1 Message Date
mbirth b7ed4ca160 Output time of last brush
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 2m56s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 3m6s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-15 01:51:15 +01:00
mbirth cd68cf9f8f Show last intensity setting
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 2m58s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 3m1s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-15 01:29:17 +01:00
mbirth 0ce5c46659 Show last brush mode
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-15 01:25:34 +01:00
mbirth 9d1faa9a23 Make var const
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-15 01:25:28 +01:00
mbirth b7fc06ce5b Fix string lengths to fit one line
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-15 01:21:12 +01:00
mbirth 58edc59ae6 Move UID and MFG below usage data and separate usage from internals
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 3m5s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 2m55s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-15 01:16:00 +01:00
mbirth 8cd7da7949 Show brush type instead of "NTAG213" header
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 3m15s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 3m12s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-15 01:05:07 +01:00
mbirth 4a65c603c8 Copy mode and intensity settings before reset
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-15 00:44:05 +01:00
mbirth 8efc64f611 Fix text alignment
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 2m8s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 2m3s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-14 12:03:59 +01:00
mbirth 39c2615350 make run
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 2m0s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 1m51s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-14 11:56:12 +01:00
mbirth 82d74ca85b Updated CHANGELOG
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 2m11s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 1m51s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-14 11:12:24 +01:00
mbirth e1e4f808b7 Updated README
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 1m53s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 1m53s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-08 21:42:15 +01:00
mbirth cd43cba450 Updated README
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 2m0s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 2m32s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-08 19:04:28 +01:00
mbirth e7f7a10d75 Updated README
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 1m53s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 2m2s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-08 18:59:45 +01:00
mbirth 15a59b7960 Added Thanks to README
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 1m54s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 2m1s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-08 18:54:10 +01:00
mbirth ab1c0bab33 Attach .fap files directly to build process
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 2m0s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 2m2s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-08 17:09:45 +01:00
mbirth 9f1709bb32 Downgrade actions/upload-artifact from v4 to v3
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Successful in 2m18s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Successful in 1m58s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-08 16:18:03 +01:00
mbirth aa4327fe1c Test Act runner build
FAP: Build for multiple SDK sources / ufbt: Build for dev channel (push) Failing after 6m55s
FAP: Build for multiple SDK sources / ufbt: Build for release channel (push) Canceled after 38s
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-08 15:59:46 +01:00
mbirth e5f8022077 Add Makefile - just for fun
Signed-off-by: Markus Birth <markus@birth-online.de>
2026-08-08 15:49:24 +01:00
Michael Huber 8074cd4ad8 [PATCH] Add usage counter reset with AUTHLIM warning
Adds the ability to reset the brush head's usage counter to zero.

Flow: Read head -> press Right ("Reset") -> confirm warning dialog ->
hold head to Flipper again. The app authenticates with the derived
password via the poller's AuthRequest event and writes the official
reset payload (00 00 02 00) to page 0x24.

The confirm dialog warns about the NTAG213 AUTHLIM feature which
permanently locks the tag after 3 wrong password attempts.
2026-08-08 15:45:58 +01:00
11 changed files with 507 additions and 22 deletions
+43
View File
@@ -0,0 +1,43 @@
name: "FAP: Build for multiple SDK sources"
# This will build your app for dev and release channels on GitHub.
# It will also build your app every day to make sure it's up to date with the latest SDK changes.
# See https://github.com/marketplace/actions/build-flipper-application-package-fap for more information
on:
push:
## put your main branch name under "branches"
branches:
- master
pull_request:
schedule:
# do a build every day
#- cron: "1 1 * * *"
jobs:
ufbt-build:
name: 'ufbt: Build for ${{ matrix.name }}'
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: dev channel
sdk-channel: dev
- name: release channel
sdk-channel: release
# You can add unofficial channels here. See ufbt action docs for more info.
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build with ufbt
uses: flipperdevices/flipperzero-ufbt-action@v0.1
id: build-app
with:
sdk-channel: ${{ matrix.sdk-channel }}
sdk-index-url: ${{ matrix.sdk-index.url }}
- name: Upload app artifacts
uses: actions/upload-artifact@v3
with:
# See ufbt action docs for other output variables
name: uk_mbirth_sonicare-${{ steps.build-app.outputs.suffix }}.fap
path: ${{ steps.build-app.outputs.fap-artifacts }}
archive: false
+5 -1
View File
@@ -1,2 +1,6 @@
v0.9:
v1.0 - 2026-08-03:
- Add: Reset usage counter (with AUTHLIM warning dialog)
Thanks to Michael Huber
v0.9 - 2026-03-31:
Initial Release
+14
View File
@@ -0,0 +1,14 @@
.PHONY: all
all:
ufbt
.PHONY: run
run:
ufbt launch
.PHONY: clean
clean:
rm -rf ./dist/*
rm -rf ~/.ufbt/build/uk_mbirth_sonicare*
+27 -1
View File
@@ -21,9 +21,35 @@ Features
This Flipper Zero app will scan the NFC chip and show the UID, MFG code, lifespan, as well as
the used time. It'll also generate the NFC password.
It can also **reset the usage counter** of the brush head. After reading a head, press the
right button ("Reset"), confirm the warning dialog, and hold the head to the Flipper again.
> **Warning:** The NTAG213 in the brush heads permanently disables all write access after
> **3 wrong password attempts** (AUTHLIM). The password is derived correctly from UID + MFG,
> but if authentication fails for any reason, do NOT retry more than twice.
Download/Install
----------------
[Releases page](https://git.mbirth.uk/flipper_zero/sonicare/releases)
[Flipper Lab](https://lab.flipper.net/apps/uk_mbirth_sonicare)
Thank You
---------
* Michael Huber - reset feature
* Vilém Kužel - also reset feature (a few days after Michael)
TODO
----
* verify NTAG213 data to be a Sonicare brush head (check NDEF contents and MFG regex?)
* NDEF might point to different Philips URLs - see https://forum.dangerousthings.com/t/weirdest-nfc-device-ive-seen/11430/3
* determine brush head type from [data](https://blog.mbirth.uk/2026/03/29/sonicare-brush-head-nfc-data.html)
* allow writing back modified data (different type, reset usage)
* show last mode and intensity setting
* show last timestamp if available
* allow writing other data (different type, different settings, different timestamp)
* to test: 1B:pp:pp:pp:pp,A2:gg:dd:dd:dd:dd (p=password, g=page, d=data)
+2 -2
View File
@@ -9,9 +9,9 @@ App(
fap_category="NFC",
# Optional values
requires=["gui"],
fap_version="0.9",
fap_version="1.0",
fap_icon="uk_mbirth_sonicare.png", # 10x10 1-bit PNG
fap_description="Philips Sonicare brush head NFC reader",
fap_description="Philips Sonicare brush head NFC reader + usage reset",
fap_author="mbirth.uk",
fap_weburl="https://git.mbirth.uk/flipper_zero/sonicare",
fap_icon_assets="images", # Image assets to compile for this application
+3
View File
@@ -2,3 +2,6 @@ ADD_SCENE(sonicare, start, Start)
ADD_SCENE(sonicare, about, About)
ADD_SCENE(sonicare, read, Read)
ADD_SCENE(sonicare, read_complete, ReadComplete)
ADD_SCENE(sonicare, reset_confirm, ResetConfirm)
ADD_SCENE(sonicare, reset, Reset)
ADD_SCENE(sonicare, reset_complete, ResetComplete)
+125 -18
View File
@@ -5,6 +5,7 @@
#include <gui/modules/widget_elements/widget_element.h>
#include <gui/scene_manager.h>
#include <gui/view_dispatcher.h>
#include <datetime/datetime.h>
#include <uk_mbirth_sonicare_icons.h>
#include <dolphin/dolphin.h>
@@ -34,27 +35,53 @@ void sonicare_scene_read_complete_on_enter(void* context) {
widget_reset(widget);
const NfcDevice* nfc_device = app->nfc_device;
FURI_LOG_D("sonicare_scene_read_complete", "Pulling Mifare Ultralight data from NFC device");
const MfUltralightData* ul_data = app->nfc_data;
FURI_LOG_D("sonicare_scene_read_complete", "Alloc'ing temp_str for output");
FuriString* temp_str = furi_string_alloc();
// Brush type and colour
const uint8_t brush_id = ul_data->page[0x1f].data[2];
const char* brush_names[] = {
"Unknown brush: 0x00",
"Prem. Plaque Def., Wt",
"Prem. Plaque Defence, B",
"Premium Gum Care, Wt",
"Premium Gum Care, Bk",
"Premium White, White",
"Premium White, Black",
"Opt. Plaque Defence, Wt",
"Optimal Gum Care, Wt", // 0x08
"Optimal White, White",
"Optimal White, Black",
"Opt. White (small), Wt",
"InterCare, White",
"InterCare (small), Wt",
"TongueCare+, White",
"TongueCare+, Black",
"A3 Prem. All-in-One, Wt", // 0x10
"A3 Prem. All-in-One, Bk",
"SimplyClean, White",
"ProResults, White",
"Sensitive, White",
"Sensitive, Black",
"Gentle Clean, White"
};
furi_string_cat_printf(temp_str, "\e#%s\n", nfc_device_get_name(nfc_device, NfcDeviceNameTypeFull));
// UID
furi_string_cat_printf(temp_str, "UID:");
format_bytes(temp_str, ul_data->iso14443_3a_data->uid, ul_data->iso14443_3a_data->uid_len);
furi_string_cat_printf(temp_str, "\n");
// Manufacturing Code
furi_string_cat_str(temp_str, "MFG: ");
FuriString* serial_no = furi_string_alloc();
furi_string_cat_str(serial_no, (char*)(ul_data->page[0x21].data));
furi_string_right(serial_no, 2);
furi_string_cat(temp_str, serial_no);
furi_string_cat_printf(temp_str, "\n");
const int brush_names_max = sizeof(brush_names)/sizeof(brush_names[0]);
/* DEBUG STRING LENGTHS
for (int i=0; i<brush_names_max; i++) {
furi_string_cat_printf(temp_str, "\e#%s\n", brush_names[i]);
}
*/
if (brush_id < brush_names_max) {
furi_string_cat_printf(temp_str, "\e#%s\n", brush_names[brush_id]);
} else {
furi_string_cat_printf(temp_str, "\e#Unknown brush: 0x%02x\n", brush_id);
}
// Usage
uint16_t seconds = ul_data->page[0x24].data[1]*256 + ul_data->page[0x24].data[0];
@@ -83,6 +110,81 @@ void sonicare_scene_read_complete_on_enter(void* context) {
// TODO: Maybe show "replace soon" marker if 170 brushes or more?
// TODO: Maybe show "replace head" marker if 180 brushes or more?
// Brush Mode
const char* brush_modes[] = {
"Clean",
"White+",
"Gum Health",
"Deep Clean+",
"Sensitive"
};
const uint8_t brush_mode = ul_data->page[0x24].data[3];
if (brush_mode<(sizeof(brush_modes)/sizeof(brush_modes[0]))) {
furi_string_cat_printf(temp_str, "Last mode: %s\n", brush_modes[brush_mode]);
} else {
furi_string_cat_printf(temp_str, "Unknown last mode: 0x%02x", brush_mode);
}
// Brush Intensity
const char* brush_intensities[] = {
"Low",
"Med",
"High"
};
const uint8_t brush_intensity = ul_data->page[0x24].data[2];
if (brush_intensity<(sizeof(brush_intensities)/sizeof(brush_intensities[0]))) {
furi_string_cat_printf(temp_str, "Last intensity: %s\n", brush_intensities[brush_intensity]);
} else {
furi_string_cat_printf(temp_str, "Unknown last intensity: 0x%02x", brush_intensity);
}
// Last brush timestamp
uint32_t unixtime =
ul_data->page[0x26].data[3] * 16777216
+ ul_data->page[0x26].data[2] * 65536
+ ul_data->page[0x26].data[1] * 256
+ ul_data->page[0x26].data[0];
furi_string_cat_printf(temp_str, "Last brush: %li\n", unixtime);
if (unixtime > 0) {
DateTime brush_dt;
datetime_timestamp_to_datetime(unixtime, &brush_dt);
furi_string_cat_printf(
temp_str,
"%04i-%02i-%02i %02i:%02i:%02i UTC\n",
brush_dt.year,
brush_dt.month,
brush_dt.day,
brush_dt.hour,
brush_dt.minute,
brush_dt.second
);
}
furi_string_cat_str(temp_str, "____________________\n");
// UID
furi_string_cat_str(temp_str, "UID:");
format_bytes(temp_str, ul_data->iso14443_3a_data->uid, ul_data->iso14443_3a_data->uid_len);
furi_string_cat_str(temp_str, "\n");
// Cache UID + MFG for the reset scene's password derivation.
// MFG (10 bytes, e.g. "221214 12K") = page[0x21].data[2..3] +
// page[0x22].data[0..3] + page[0x23].data[0..3]
// (first 2 bytes of page 0x21 are the max-usage value, not MFG).
memcpy(app->sonicare_uid, ul_data->iso14443_3a_data->uid, 7);
memcpy(app->sonicare_mfg + 0, ul_data->page[0x21].data + 2, 2);
memcpy(app->sonicare_mfg + 2, ul_data->page[0x22].data, 4);
memcpy(app->sonicare_mfg + 6, ul_data->page[0x23].data, 4);
// Manufacturing Code
furi_string_cat_str(temp_str, "MFG: ");
FuriString* serial_no = furi_string_alloc();
furi_string_cat_str(serial_no, (char*)(ul_data->page[0x21].data));
furi_string_right(serial_no, 2);
furi_string_cat(temp_str, serial_no);
furi_string_cat_printf(temp_str, "\n");
// NFC password
uint32_t unlock_pwd_big = get_sonicare_password((uint8_t*)ul_data->iso14443_3a_data->uid, (uint8_t*)furi_string_get_cstr(serial_no));
FURI_LOG_D("sonicare_scene_read_complete", "NFC unlock password: 0x%08lx", unlock_pwd_big);
@@ -98,18 +200,23 @@ void sonicare_scene_read_complete_on_enter(void* context) {
furi_string_free(temp_str);
furi_string_free(serial_no);
// TODO: widget_add_button_element(widget, GuiButtonTypeRight, "Change", sonicare_scene_read_complete_widget_callback, app);
widget_add_button_element(
widget,
GuiButtonTypeRight,
"Reset",
sonicare_scene_read_complete_widget_callback,
app);
view_dispatcher_switch_to_view(app->view_dispatcher, SonicareViewWidget);
}
bool sonicare_scene_read_complete_on_event(void* context, SceneManagerEvent event) {
UNUSED(context);
Sonicare* app = context;
bool consumed = false;
if (event.type == SceneManagerEventTypeCustom) {
if (event.event == GuiButtonTypeRight) {
// switch to edit screen
scene_manager_next_scene(app->scene_manager, SonicareSceneResetConfirm);
consumed = true;
}
} else if (event.type == SceneManagerEventTypeBack) {
+128
View File
@@ -0,0 +1,128 @@
#include "../uk_mbirth_sonicare.h"
#include "../sonicare_password.h"
#include <uk_mbirth_sonicare_icons.h>
#include <gui/scene_manager.h>
#include <gui/view_dispatcher.h>
#include <nfc/nfc.h>
#include <nfc/nfc_device.h>
#include <nfc/protocols/mf_ultralight/mf_ultralight_poller.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <dolphin/dolphin.h>
#define SONICARE_PAGE_USAGE 0x24
static NfcCommand sonicare_scene_reset_poller_callback(NfcGenericEvent event, void* context) {
furi_assert(context);
Sonicare* app = context;
if(event.protocol != NfcProtocolMfUltralight) {
return NfcCommandContinue;
}
const MfUltralightPollerEvent* ev = event.event_data;
NfcCommand command = NfcCommandContinue;
if(ev->type == MfUltralightPollerEventTypeRequestMode) {
ev->data->poller_mode = MfUltralightPollerModeRead;
} else if(ev->type == MfUltralightPollerEventTypeAuthRequest) {
// The poller asks us for the password before reading protected pages.
// Provide it (MSB-first, as in the 8-digit hex string).
uint32_t pwd = get_sonicare_password(app->sonicare_uid, app->sonicare_mfg);
FURI_LOG_I("sonicare_scene_reset", "AuthRequest - providing password %08lX", pwd);
ev->data->auth_context.password.data[0] = (pwd >> 24) & 0xFF;
ev->data->auth_context.password.data[1] = (pwd >> 16) & 0xFF;
ev->data->auth_context.password.data[2] = (pwd >> 8) & 0xFF;
ev->data->auth_context.password.data[3] = (pwd >> 0) & 0xFF;
ev->data->auth_context.skip_auth = false;
} else if(ev->type == MfUltralightPollerEventTypeAuthSuccess) {
FURI_LOG_I("sonicare_scene_reset", "Auth success, writing page 0x24");
const MfUltralightData* ul_data = app->nfc_data;
MfUltralightPoller* poller = (MfUltralightPoller*)event.instance;
// Official reset: write page 0x24 = {00 00 02 00}
MfUltralightPage page = {.data = {0x00, 0x00, 0x02, 0x00}};
// copy mode and intensity settings
page.data[2] = ul_data->page[SONICARE_PAGE_USAGE].data[2];
page.data[3] = ul_data->page[SONICARE_PAGE_USAGE].data[3];
MfUltralightError err =
mf_ultralight_poller_write_page(poller, SONICARE_PAGE_USAGE, &page);
if(err == MfUltralightErrorNone) {
FURI_LOG_I("sonicare_scene_reset", "Usage counter reset OK");
app->reset_state = SonicareResetStateSuccess;
} else {
FURI_LOG_E("sonicare_scene_reset", "Write failed: %d", err);
app->reset_state = SonicareResetStateFailedWrite;
}
view_dispatcher_send_custom_event(app->view_dispatcher, NfcCustomEventWorkerExit);
command = NfcCommandStop;
} else if(ev->type == MfUltralightPollerEventTypeAuthFailed) {
FURI_LOG_E("sonicare_scene_reset", "Auth failed (poller state machine)");
app->reset_state = SonicareResetStateFailedAuth;
view_dispatcher_send_custom_event(app->view_dispatcher, NfcCustomEventWorkerExit);
command = NfcCommandStop;
} else if(ev->type == MfUltralightPollerEventTypeReadSuccess) {
// Card has no password protection or auth was skipped; try direct write.
FURI_LOG_I("sonicare_scene_reset", "ReadSuccess without prior auth, trying write");
MfUltralightPoller* poller = (MfUltralightPoller*)event.instance;
MfUltralightPage page = {.data = {0x00, 0x00, 0x02, 0x00}};
MfUltralightError err =
mf_ultralight_poller_write_page(poller, SONICARE_PAGE_USAGE, &page);
app->reset_state =
(err == MfUltralightErrorNone) ? SonicareResetStateSuccess : SonicareResetStateFailedWrite;
view_dispatcher_send_custom_event(app->view_dispatcher, NfcCustomEventWorkerExit);
command = NfcCommandStop;
}
return command;
}
void sonicare_scene_reset_on_enter(void* context) {
Sonicare* app = context;
Popup* popup = app->popup;
app->reset_state = SonicareResetStateInit;
popup_reset(popup);
popup_set_header(popup, "Resetting", 83, 8, AlignCenter, AlignTop);
popup_set_text(popup, "Hold brush stem\nnext to\nFlipper's back", 83, 27, AlignCenter, AlignTop);
popup_set_icon(app->popup, 0, 0, &I_sonicare_read);
view_dispatcher_switch_to_view(app->view_dispatcher, SonicareViewPopup);
app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfUltralight);
nfc_poller_start(app->poller, sonicare_scene_reset_poller_callback, app);
}
bool sonicare_scene_reset_on_event(void* context, SceneManagerEvent event) {
Sonicare* app = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == NfcCustomEventWorkerExit) {
if(app->reset_state == SonicareResetStateSuccess) {
notification_message(app->notifications, &sequence_success);
dolphin_deed(DolphinDeedNfcRead);
} else {
notification_message(app->notifications, &sequence_error);
}
scene_manager_next_scene(app->scene_manager, SonicareSceneResetComplete);
consumed = true;
}
}
return consumed;
}
void sonicare_scene_reset_on_exit(void* context) {
Sonicare* app = context;
nfc_poller_stop(app->poller);
nfc_poller_free(app->poller);
popup_reset(app->popup);
}
+80
View File
@@ -0,0 +1,80 @@
#include "../uk_mbirth_sonicare.h"
#include <gui/canvas.h>
#include <gui/modules/widget.h>
#include <gui/scene_manager.h>
#include <gui/view_dispatcher.h>
#include <uk_mbirth_sonicare_icons.h>
#include <dolphin/dolphin.h>
static void sonicare_scene_reset_complete_widget_callback(
GuiButtonType result,
InputType type,
void* context) {
furi_assert(context);
Sonicare* app = context;
if(type == InputTypeShort) {
view_dispatcher_send_custom_event(app->view_dispatcher, result);
}
}
void sonicare_scene_reset_complete_on_enter(void* context) {
Sonicare* app = context;
Widget* widget = app->widget;
widget_reset(widget);
if(app->reset_state == SonicareResetStateSuccess) {
widget_add_icon_element(widget, 0, 0, &I_sonicare_brush);
widget_add_string_element(
widget, 64, 20, AlignCenter, AlignCenter, FontPrimary, "Counter reset!");
widget_add_string_element(
widget,
64,
40,
AlignCenter,
AlignCenter,
FontSecondary,
"Read again to verify");
} else {
const char* reason =
(app->reset_state == SonicareResetStateFailedAuth) ? "Auth failed" : "Write failed";
widget_add_string_element(
widget, 64, 20, AlignCenter, AlignCenter, FontPrimary, "Reset failed");
widget_add_string_element(
widget, 64, 40, AlignCenter, AlignCenter, FontSecondary, reason);
}
widget_add_button_element(
widget,
GuiButtonTypeLeft,
"Back",
sonicare_scene_reset_complete_widget_callback,
app);
view_dispatcher_switch_to_view(app->view_dispatcher, SonicareViewWidget);
}
bool sonicare_scene_reset_complete_on_event(void* context, SceneManagerEvent event) {
Sonicare* app = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == GuiButtonTypeLeft) {
// Go back to start menu so user can re-read the head
scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, SonicareSceneStart);
consumed = true;
}
} else if(event.type == SceneManagerEventTypeBack) {
scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, SonicareSceneStart);
consumed = true;
}
return consumed;
}
void sonicare_scene_reset_complete_on_exit(void* context) {
Sonicare* app = context;
widget_reset(app->widget);
}
+68
View File
@@ -0,0 +1,68 @@
#include "../uk_mbirth_sonicare.h"
#include <gui/canvas.h>
#include <gui/modules/widget.h>
#include <gui/scene_manager.h>
#include <gui/view_dispatcher.h>
#include <uk_mbirth_sonicare_icons.h>
#include <dolphin/dolphin.h>
static void sonicare_scene_reset_confirm_widget_callback(
GuiButtonType result,
InputType type,
void* context) {
furi_assert(context);
Sonicare* app = context;
if(type == InputTypeShort) {
view_dispatcher_send_custom_event(app->view_dispatcher, result);
}
}
void sonicare_scene_reset_confirm_on_enter(void* context) {
Sonicare* app = context;
Widget* widget = app->widget;
widget_reset(widget);
widget_add_string_element(
widget, 64, 2, AlignCenter, AlignTop, FontPrimary, "Reset usage counter?");
widget_add_text_box_element(
widget,
0,
14,
128,
38,
AlignLeft,
AlignTop,
"\e#Warning:\e# The NTAG213 per-\nmanently locks after 3 wrong\npassword attempts.",
false);
widget_add_button_element(
widget, GuiButtonTypeLeft, "Cancel", sonicare_scene_reset_confirm_widget_callback, app);
widget_add_button_element(
widget, GuiButtonTypeRight, "Reset", sonicare_scene_reset_confirm_widget_callback, app);
view_dispatcher_switch_to_view(app->view_dispatcher, SonicareViewWidget);
}
bool sonicare_scene_reset_confirm_on_event(void* context, SceneManagerEvent event) {
Sonicare* app = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == GuiButtonTypeRight) {
scene_manager_next_scene(app->scene_manager, SonicareSceneReset);
consumed = true;
} else if(event.event == GuiButtonTypeLeft) {
consumed = scene_manager_previous_scene(app->scene_manager);
}
} else if(event.type == SceneManagerEventTypeBack) {
consumed = scene_manager_previous_scene(app->scene_manager);
}
return consumed;
}
void sonicare_scene_reset_confirm_on_exit(void* context) {
Sonicare* app = context;
widget_reset(app->widget);
}
+12
View File
@@ -32,6 +32,13 @@
typedef struct Sonicare Sonicare;
typedef enum {
SonicareResetStateInit,
SonicareResetStateSuccess,
SonicareResetStateFailedAuth,
SonicareResetStateFailedWrite,
} SonicareResetState;
struct Sonicare {
ViewDispatcher* view_dispatcher;
Gui* gui;
@@ -54,6 +61,11 @@ struct Sonicare {
NfcListener* listener;
NfcDevice* nfc_device;
MfUltralightData* nfc_data;
// Reset flow
SonicareResetState reset_state;
uint8_t sonicare_uid[7];
uint8_t sonicare_mfg[10];
};
typedef enum {