Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7ed4ca160
|
||
|
|
cd68cf9f8f
|
||
|
|
0ce5c46659
|
||
|
|
9d1faa9a23
|
||
|
|
b7fc06ce5b
|
||
|
|
58edc59ae6
|
||
|
|
8cd7da7949
|
||
|
|
4a65c603c8
|
||
|
|
8efc64f611
|
||
|
|
39c2615350
|
||
|
|
82d74ca85b
|
||
|
|
e1e4f808b7
|
||
|
|
cd43cba450
|
||
|
|
e7f7a10d75
|
||
|
|
15a59b7960
|
||
|
|
ab1c0bab33
|
||
|
|
9f1709bb32
|
||
|
|
aa4327fe1c
|
||
|
|
e5f8022077
|
||
|
|
8074cd4ad8
|
@@ -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
@@ -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
|
Initial Release
|
||||||
|
|||||||
@@ -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*
|
||||||
|
|
||||||
@@ -21,9 +21,35 @@ Features
|
|||||||
This Flipper Zero app will scan the NFC chip and show the UID, MFG code, lifespan, as well as
|
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.
|
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
|
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)
|
* 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
@@ -9,9 +9,9 @@ App(
|
|||||||
fap_category="NFC",
|
fap_category="NFC",
|
||||||
# Optional values
|
# Optional values
|
||||||
requires=["gui"],
|
requires=["gui"],
|
||||||
fap_version="0.9",
|
fap_version="1.0",
|
||||||
fap_icon="uk_mbirth_sonicare.png", # 10x10 1-bit PNG
|
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_author="mbirth.uk",
|
||||||
fap_weburl="https://git.mbirth.uk/flipper_zero/sonicare",
|
fap_weburl="https://git.mbirth.uk/flipper_zero/sonicare",
|
||||||
fap_icon_assets="images", # Image assets to compile for this application
|
fap_icon_assets="images", # Image assets to compile for this application
|
||||||
|
|||||||
@@ -2,3 +2,6 @@ ADD_SCENE(sonicare, start, Start)
|
|||||||
ADD_SCENE(sonicare, about, About)
|
ADD_SCENE(sonicare, about, About)
|
||||||
ADD_SCENE(sonicare, read, Read)
|
ADD_SCENE(sonicare, read, Read)
|
||||||
ADD_SCENE(sonicare, read_complete, ReadComplete)
|
ADD_SCENE(sonicare, read_complete, ReadComplete)
|
||||||
|
ADD_SCENE(sonicare, reset_confirm, ResetConfirm)
|
||||||
|
ADD_SCENE(sonicare, reset, Reset)
|
||||||
|
ADD_SCENE(sonicare, reset_complete, ResetComplete)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <gui/modules/widget_elements/widget_element.h>
|
#include <gui/modules/widget_elements/widget_element.h>
|
||||||
#include <gui/scene_manager.h>
|
#include <gui/scene_manager.h>
|
||||||
#include <gui/view_dispatcher.h>
|
#include <gui/view_dispatcher.h>
|
||||||
|
#include <datetime/datetime.h>
|
||||||
#include <uk_mbirth_sonicare_icons.h>
|
#include <uk_mbirth_sonicare_icons.h>
|
||||||
#include <dolphin/dolphin.h>
|
#include <dolphin/dolphin.h>
|
||||||
|
|
||||||
@@ -34,27 +35,53 @@ void sonicare_scene_read_complete_on_enter(void* context) {
|
|||||||
|
|
||||||
widget_reset(widget);
|
widget_reset(widget);
|
||||||
|
|
||||||
const NfcDevice* nfc_device = app->nfc_device;
|
|
||||||
FURI_LOG_D("sonicare_scene_read_complete", "Pulling Mifare Ultralight data from NFC device");
|
FURI_LOG_D("sonicare_scene_read_complete", "Pulling Mifare Ultralight data from NFC device");
|
||||||
const MfUltralightData* ul_data = app->nfc_data;
|
const MfUltralightData* ul_data = app->nfc_data;
|
||||||
|
|
||||||
FURI_LOG_D("sonicare_scene_read_complete", "Alloc'ing temp_str for output");
|
FURI_LOG_D("sonicare_scene_read_complete", "Alloc'ing temp_str for output");
|
||||||
FuriString* temp_str = furi_string_alloc();
|
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));
|
const int brush_names_max = sizeof(brush_names)/sizeof(brush_names[0]);
|
||||||
|
|
||||||
// UID
|
/* DEBUG STRING LENGTHS
|
||||||
furi_string_cat_printf(temp_str, "UID:");
|
for (int i=0; i<brush_names_max; i++) {
|
||||||
format_bytes(temp_str, ul_data->iso14443_3a_data->uid, ul_data->iso14443_3a_data->uid_len);
|
furi_string_cat_printf(temp_str, "\e#%s\n", brush_names[i]);
|
||||||
furi_string_cat_printf(temp_str, "\n");
|
}
|
||||||
|
*/
|
||||||
// Manufacturing Code
|
|
||||||
furi_string_cat_str(temp_str, "MFG: ");
|
if (brush_id < brush_names_max) {
|
||||||
FuriString* serial_no = furi_string_alloc();
|
furi_string_cat_printf(temp_str, "\e#%s\n", brush_names[brush_id]);
|
||||||
furi_string_cat_str(serial_no, (char*)(ul_data->page[0x21].data));
|
} else {
|
||||||
furi_string_right(serial_no, 2);
|
furi_string_cat_printf(temp_str, "\e#Unknown brush: 0x%02x\n", brush_id);
|
||||||
furi_string_cat(temp_str, serial_no);
|
}
|
||||||
furi_string_cat_printf(temp_str, "\n");
|
|
||||||
|
|
||||||
// Usage
|
// Usage
|
||||||
uint16_t seconds = ul_data->page[0x24].data[1]*256 + ul_data->page[0x24].data[0];
|
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 soon" marker if 170 brushes or more?
|
||||||
// TODO: Maybe show "replace head" marker if 180 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
|
// 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));
|
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);
|
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(temp_str);
|
||||||
furi_string_free(serial_no);
|
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);
|
view_dispatcher_switch_to_view(app->view_dispatcher, SonicareViewWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sonicare_scene_read_complete_on_event(void* context, SceneManagerEvent event) {
|
bool sonicare_scene_read_complete_on_event(void* context, SceneManagerEvent event) {
|
||||||
UNUSED(context);
|
Sonicare* app = context;
|
||||||
|
|
||||||
bool consumed = false;
|
bool consumed = false;
|
||||||
|
|
||||||
if (event.type == SceneManagerEventTypeCustom) {
|
if (event.type == SceneManagerEventTypeCustom) {
|
||||||
if (event.event == GuiButtonTypeRight) {
|
if (event.event == GuiButtonTypeRight) {
|
||||||
// switch to edit screen
|
scene_manager_next_scene(app->scene_manager, SonicareSceneResetConfirm);
|
||||||
consumed = true;
|
consumed = true;
|
||||||
}
|
}
|
||||||
} else if (event.type == SceneManagerEventTypeBack) {
|
} else if (event.type == SceneManagerEventTypeBack) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -32,6 +32,13 @@
|
|||||||
|
|
||||||
typedef struct Sonicare Sonicare;
|
typedef struct Sonicare Sonicare;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SonicareResetStateInit,
|
||||||
|
SonicareResetStateSuccess,
|
||||||
|
SonicareResetStateFailedAuth,
|
||||||
|
SonicareResetStateFailedWrite,
|
||||||
|
} SonicareResetState;
|
||||||
|
|
||||||
struct Sonicare {
|
struct Sonicare {
|
||||||
ViewDispatcher* view_dispatcher;
|
ViewDispatcher* view_dispatcher;
|
||||||
Gui* gui;
|
Gui* gui;
|
||||||
@@ -54,6 +61,11 @@ struct Sonicare {
|
|||||||
NfcListener* listener;
|
NfcListener* listener;
|
||||||
NfcDevice* nfc_device;
|
NfcDevice* nfc_device;
|
||||||
MfUltralightData* nfc_data;
|
MfUltralightData* nfc_data;
|
||||||
|
|
||||||
|
// Reset flow
|
||||||
|
SonicareResetState reset_state;
|
||||||
|
uint8_t sonicare_uid[7];
|
||||||
|
uint8_t sonicare_mfg[10];
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
|||||||
Reference in New Issue
Block a user