From 9cd0592aafb826ea08d180c857d399acdf607706 Mon Sep 17 00:00:00 2001 From: Skorpionm <85568270+Skorpionm@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:31:41 +0400 Subject: [PATCH 01/14] SubGhz: add keeloq potocol JCM_Tech (#1939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SubGhz: add keeloq potocol JCM_Tech * SubGhz: add new metod decoder Co-authored-by: あく --- assets/resources/subghz/assets/keeloq_mfcodes | 99 ++++++++++--------- lib/subghz/protocols/keeloq.c | 18 ++++ lib/subghz/protocols/keeloq_common.c | 30 +++++- lib/subghz/protocols/keeloq_common.h | 18 ++++ 4 files changed, 117 insertions(+), 48 deletions(-) diff --git a/assets/resources/subghz/assets/keeloq_mfcodes b/assets/resources/subghz/assets/keeloq_mfcodes index b8fc36903..1b27bfb01 100644 --- a/assets/resources/subghz/assets/keeloq_mfcodes +++ b/assets/resources/subghz/assets/keeloq_mfcodes @@ -1,50 +1,55 @@ Filetype: Flipper SubGhz Keystore File Version: 0 Encryption: 1 -IV: 2A 34 F1 5A AF 6F F5 1A 83 A6 1E DA DE B7 3D F1 -06B63DF24AE073A2F2B19C55CA9E8364FBECD26E49C551990153F6513BDE5267 -6139C78C74C341EB7474085CF1D047BD6FB005F80A72AF3EF3F89D58EF5DF500 -D85F11689020ECA47FBE9C2B67EE41A81E1F06DE2A35AF958965E3ECE29EA701 -1AE9073A42FE0E439544FE6945F6B33CF15A7A4A279020B5E0B3BE33FD189A7E -E161F007854BB33E0056FA09A2E2DEE66789B5C87C8D6D3DE2C8C1BD2B48983EB9D1C5697CA6E95996918F7C47B761B0 -59AE4644DCB3D720C38B5115F230DA58E7BE0A697907F6174BB05AB7886ACDB1 -634DF0BCC185C4C1F7E1B1594B4438D051ABAE092433078963063B51D961D08C -1EBEBCB49E498B9BE977D53EC21B9A546155B627737BD0AA832D496035729346 -4DFA93E639197772D57E8ACE04512CEFC045B8CC965C175A25ED525B630CBB63 -C2D5235D1014A319B249EAE8A5EE350F18D5AB8A498EF222704BD4EB1435F388 -F66D1937160E1392197F463A52E87FCE938A92070892113443C348D7553327A5715CF615CE2F2C96284F47759E043419 -841D29E7CBE040188E2283BFBA9F26EF2F65CCB085B56C3515E8C46C3F20BD75BAA963550869435FDAF509CEEE66A2C4 -7D87E24487D307635E7A17B989B8547EE11F3BF3468D055F0B44633B631BA42C -B4916043973501B95A82B329196D6EBA69FBBC3AF8FD914583104E0E18CE82F6 -E4649F9C2A5465D2EA6F3E9724DD06CD6962FE2BAEB14F1453C14D1559232AE1 -96E15D890DF7FD348441F5E429A875754C6BF0520A787F8E9D8C5415674783CC -CB52005EDED47B57F795BC92FB0522EAB18D23EE028B8D10ED57828C250EB285BFEC6E4A4BE8DABCE0D57ECAA20D90C3 -8E5A50C7D5C374445E88752301D20F0B3D6E4988B61D90FD63779B0EDEF9C60D -49D6CB276A0E5FF134A38062503F01351F44CD6455708B50B5F07D03FC477C33 -CB45B56613DF208E79E4E10A6510F07DC1AA49210C7B94E8BBAECD2C35EC6ABC99FB10FD7C96DD6BB6A6685E9FAD93FB -0743F3CC51200F763C242F1956B4D775C092ADF1A5C19ACAE96EB60C2990CF214F8FEA8FC6749286F6BDAB67657C479A -E5608B28A058787D64A145F0362DEFD98CAE0B5A0F22C6DA7C6D278C7B5F95E3 -D4C113D43E7FB6D2EFA9E87471AA76A61B26872607B4AF5B87F9D72113835CE6 -2DC502800BFD21B76126390CA64A08C5432A2254E822F214CDE1EA11430084C5 -CA22C73010B0F1CB8009601BE2AF0B3674D83D5880E4A26C2A3FF0EA0A098CEA -E53B2B102FDB000E9BB747F957156976E5A0C0E3898AA844C13AE8A9CEE7013B -95CF1A46FFC252BE92919531C92BF6A3AA1B16C170DF4461EC54BE07A55C2387 -2EC7E24090F6DFFF6F2F2D8874D2F36AA769995F31F29FBE3B0EA6A16C3EE833 -C1145B1D9AC70761EA902B86455C1BE1BB1153552A1F7327411DECABE538827B -18D596CADD2EE544200A58716C7A4690B658E58CC2B97334740F70894A6C90FA -6A2F8859DFF01E13AC6C5300AD4A2218810FC91A6FB64A560E99FE6C99226AD2 -48D2EB5A08E35AF89A3B7A1CFDEE829FC0C2DDD2E965F4E3D043B0B14CB7825E -91039325D53CDD0236D1CD13047973A013C14B45A32DE0784A73BFABCEAFBCD1 -51B4EAC87C4DC49B007F40D38B8166C388A1AF25E8D2FF6598E8EDE8726E6E14AD88443114D2A0F5E7721E304F3870DA -3A179DDF65B9868CD84C7C04931F40D5D204C97B20DCBF1A70C241E59BFD7F14 -AF538FD16104DCAF03F4DDF05026D6741898DFC247E48A8F72E652DDF2DFD289 -E67F16AEC9D84B6C06F77B806CA6FBC7618BFBECD0D7A04EC3AE1D1DD06BEC5B -FA4D9F8920EBF2F4293C6D4E99083AA4A71A9DDFFDB07EEBDC552DACEC4DA24A -5BF23E630AC81E2CD533803E225BCB3C481B8D650A9858CF2B5219BAE1CDA01A -17B57E8C1032481E69247EA9A0C9EA41F6C0EA9B3F11170CA69C0842423F0455 -96EA848B8527A647DC9DACDB16C5D92B0081EB1CD77B99B47F56C2E249190BD3BE4306333F37487133DD3AD8E57F3092 -B0E9411274D799BE5989D52E74E00DE310CCA2BD47D7A8FA554D66BB04CD787A -D0D28476E3D8832975653D93F545C35278EC1F0B7AD70CA2F36EB476CC207937 -933195E37014619F997B73F5CF4C0110865A822CA8CB0ED1D977D49A1B06A37F -E790CAC2A26452BF941A9E1BABF0A85598EA1CC8F8CFED637C9B40D5E027B518 -49C1F179ABA5BD4F2C45257A33701730E9CC4728677EFF07808ABE31D3CE6FD5C805F43EA5ABB7261B220C82F0794092 +IV: AA FF DE 54 A1 BB F1 21 83 46 FE 2A 1E B7 3D 33 +95B8CD65BBAC95EACE67CA94F679B82877A921396D461ECB479722F8A369454A +61065C41297B9FF8F8168814F49A03D1FE7B4CB79DFFCBBF0402AAA6A2211E84 +A1557AC139188FF105D1081A4B688C5CA440FB5DA7F40901B541120AD08A544F +AF0A6056D7F0D97DAD6C16C4E63204E4B3B1C5A20AC82B983B516F4F718EE29F +6861BFAE46A1AADB1DB2D6DFAA7E39D21D5B3E46A41BD50F4F2828879EB328EF0A406F2B9C79A031AB361257E6D69756 +0DDB3DAC53678541981CC46C22CED245CBA314C9BBE1BA9383B8505B75AC5E40 +99AB5D9404934F2D257ED04D9F8CCEE06D00F38157B121AFD63101E4E5C08268 +5114A6C42B342C7D933A76F9052FF963C2047E85EA524497C21B4C35C38EF6E7 +88CA2A1907D94B972FF93DBB9B88CB576F3E1BB0FE8F85A5B2CCA7D44B00374D +349C4153FE7CA8AE044E9F75F77D9694304474CE3F127CF968662B5F78A7F421 +62AA02E20CA7E691EFC0B55CA41C9BDF889FB23868289284241CD31AA1A0E499AE2A770B6B5AB3170CDCCDB8A246D36C +97901B5EB76228ADF8E5073F1BAB1502878DEFF1C4EBF12A43D105556CB7E80F947A8BD7831666BD838C57CDF64A6F3F +B05959D210B500943A93BDFAF783D9DB215FC84503B152EAFBCFB5B6237E3888 +B393DE4489BCAFD5DB80592A12E329E18913E185D2042580048029A8C4C3A257 +B4B30492A5F0C3C763E2F43C02D1451A5B9CFB468CFE62BE85B1F56FF49DAB9A +CE5D57C0EE3D717FC717EB725970A9F25D211546EE7AC5C237950CEA323D85D4 +4E9028944813FD40A17AF6DF5A97E76179B48EE79265BBD38B07E3A270587A813DADB51B3367479AC5644F754B5613F8 +3B3C3000B9D1361711ECE3DB77C90A059576F738CB167679DA36DD3D128B27A1 +997023148148DE7B9CBA47D3FD48DEF73AA1715FF4BC1E7A1DBA6D52A0DCB2C0 +C8428D18E69FB92486434FCE470F1FF37D40507F27D824679C132A70D516530367277F02DDB5C464D03450FF6B425A24 +3701200DF5DA7235971FD95844056E74C7D61A8EB12A8772E04F52037C63D50B6229A7F905F3E6F84C565FCC7632870C +BB392A464CDC0D5D923AA9EF8ECC3C6F020D0AD82165462DF0DE7C5025AAAAAC +999C82209B30638506E5D708471676D2CBB4A432E5AF86ABD61179111EDAE636 +FDE2A452A6B47261338117EC20FC57731DA492562ECD21BBC61F098A5442CF20 +D923BABB5C4DFB48E3F763898B2796C7830D3EE9A91DF904AC2223A0F4736507 +0987DDAC695DD5E4607048DF1D4EF96599E17ED52F41785E676AA048AB7213FE +26CB3E6CFA10338A8DDD99BFF6957C53DEF435CB0FF977B71B5164ADFE11292A +097908FD07A0A093CA80E6FF59524707C1A11169D0CB6F8E4967D8DAA725FE7A +8C629E70A5CC6FCB039DFA1A6AC58CB7B7E92C85BDA66266AB49E6B1285FC7A6 +39A2052350CD446EDC1B9AD0C2DD51C78B2E5F3A76AAD0EC200F74B40ACD4AC5 +A1685CF8C4A5401F2CA0C8172CB5B4B5726C61CE68A72AE834B0A472CEB2F3DE +1F5ED5793DB381D1B501BA8A4DF3E74FB11FC1A922DDC8AE62E5BA8934C37EA8 +D80EF661BF36E2F6C179E253CE5BC3732684ACBC7C65E526A628442A2EBF8FAA +7785BF721F21E19A8CFBFBBB56BD76B96A4E8EF9F8A2344009B14AB385909598F834A5533B648DA7D62BD6D4314A43A5 +C8F6F943DE615B5827569B283577344C0455B3279C73634FC4E0E9A8088DF633 +FB4F4C786FC51BBDA679A212B4A05EF120AC62F7EBFFE8263BD50A4D9BC9C6E0 +16EEC35CF69BA86DB3BE999CDF9B39F5736F3727B2AA2C5AB9141A48F176D831 +AD1AD6DE813E7710DA3AF546D4F9EA085831E6B3FA17B64F1B8765F48134EA54 +345D743BC35B4A8614632ADD11E809C0D1E6C78F9469256B9A738DA0B648B2B8 +7C876CECAC839EBB4609C3996966C3EC454F51C8ABCC51097E405370C4B6F086 +0F857C031FD3047607647148C534F969567F207FF1691D8D06DCDF4C2514695D +EC0630EDC82241C1952F49B6B1B0C1A954A7DDD6BDB1326ACC54AD449D1BF985 +286EF9F7FD0D09F2604CCE867C52144CD0C4773A3D8183066C61B8BF9860AE7C +EA55424097A08722A66966E3177E09DE91AC65175E5C68CB47B6153E6585DF85 +D54FCDF9EA4BD1FE4F316DB6D5CE4A2675F2D0144772865EDC781FBA7DFD23E4 +7A2F5C5CA9F97FE9527BAA760E64B930C407A27DE036476737E6BDD9422F4056A5F1F414F12F0982109FD7C30E8CC1CB +06BAD9B4EEEEB1BCF8C97672D271534FE84D772282EE9642698788D3842D7641 +101C1B2DBD963E23777294C22E553D145D5B40838F91355CA86D571A0CEFF68F +1B148C2B502B3E0A5BD40858E019C513DD4CCAF2A114CBB29C59BFB018079285 +8DF4D07EC20FF873EA989ACEF4AF96E9787FE6E0F71965858B4186C3AF302A31 +2317DC8C098CD60F3467B3644A19CCE887339708820CD37F6F5277D6648F837512F70CE90E23D7339CDDE002BD8D83DB diff --git a/lib/subghz/protocols/keeloq.c b/lib/subghz/protocols/keeloq.c index ae6588e7a..eef1d0937 100644 --- a/lib/subghz/protocols/keeloq.c +++ b/lib/subghz/protocols/keeloq.c @@ -529,6 +529,24 @@ static uint8_t subghz_protocol_keeloq_check_remote_controller_selector( return 1; } break; + case KEELOQ_LEARNING_MAGIC_SERIAL_TYPE_2: + man = subghz_protocol_keeloq_common_magic_serial_type2_learning( + fix, manufacture_code->key); + decrypt = subghz_protocol_keeloq_common_decrypt(hop, man); + if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { + *manufacture_name = furi_string_get_cstr(manufacture_code->name); + return 1; + } + break; + case KEELOQ_LEARNING_MAGIC_SERIAL_TYPE_3: + man = subghz_protocol_keeloq_common_magic_serial_type3_learning( + fix, manufacture_code->key); + decrypt = subghz_protocol_keeloq_common_decrypt(hop, man); + if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { + *manufacture_name = furi_string_get_cstr(manufacture_code->name); + return 1; + } + break; case KEELOQ_LEARNING_UNKNOWN: // Simple Learning decrypt = subghz_protocol_keeloq_common_decrypt(hop, manufacture_code->key); diff --git a/lib/subghz/protocols/keeloq_common.c b/lib/subghz/protocols/keeloq_common.c index 6c9bc461e..ddbf1c917 100644 --- a/lib/subghz/protocols/keeloq_common.c +++ b/lib/subghz/protocols/keeloq_common.c @@ -94,6 +94,34 @@ inline uint64_t inline uint64_t subghz_protocol_keeloq_common_magic_serial_type1_learning(uint32_t data, uint64_t man) { - return man | ((uint64_t)data << 40) | + return (man & 0xFFFFFFFF) | ((uint64_t)data << 40) | ((uint64_t)(((data & 0xff) + ((data >> 8) & 0xFF)) & 0xFF) << 32); } + +/** Magic_serial_type2 Learning + * @param data - btn+serial number (32bit) + * @param man - magic man (64bit) + * @return manufacture for this serial number (64bit) + */ + +inline uint64_t + subghz_protocol_keeloq_common_magic_serial_type2_learning(uint32_t data, uint64_t man) { + uint8_t* p = (uint8_t*)&data; + uint8_t* m = (uint8_t*)&man; + m[7] = p[0]; + m[6] = p[1]; + m[5] = p[2]; + m[4] = p[3]; + return man; +} + +/** Magic_serial_type3 Learning + * @param data - serial number (24bit) + * @param man - magic man (64bit) + * @return manufacture for this serial number (64bit) + */ + +inline uint64_t + subghz_protocol_keeloq_common_magic_serial_type3_learning(uint32_t data, uint64_t man) { + return (man & 0xFFFFFFFFFF000000) | (data & 0xFFFFFF); +} diff --git a/lib/subghz/protocols/keeloq_common.h b/lib/subghz/protocols/keeloq_common.h index 448388f0a..df3d0dbf3 100644 --- a/lib/subghz/protocols/keeloq_common.h +++ b/lib/subghz/protocols/keeloq_common.h @@ -22,6 +22,8 @@ #define KEELOQ_LEARNING_SECURE 3u #define KEELOQ_LEARNING_MAGIC_XOR_TYPE_1 4u #define KEELOQ_LEARNING_MAGIC_SERIAL_TYPE_1 5u +#define KEELOQ_LEARNING_MAGIC_SERIAL_TYPE_2 6u +#define KEELOQ_LEARNING_MAGIC_SERIAL_TYPE_3 7u /** * Simple Learning Encrypt @@ -72,3 +74,19 @@ uint64_t subghz_protocol_keeloq_common_magic_xor_type1_learning(uint32_t data, u */ uint64_t subghz_protocol_keeloq_common_magic_serial_type1_learning(uint32_t data, uint64_t man); + +/** Magic_serial_type2 Learning + * @param data - btn+serial number (32bit) + * @param man - magic man (64bit) + * @return manufacture for this serial number (64bit) + */ + +uint64_t subghz_protocol_keeloq_common_magic_serial_type2_learning(uint32_t data, uint64_t man); + +/** Magic_serial_type3 Learning + * @param data - btn+serial number (32bit) + * @param man - magic man (64bit) + * @return manufacture for this serial number (64bit) + */ + +uint64_t subghz_protocol_keeloq_common_magic_serial_type3_learning(uint32_t data, uint64_t man); From 4b921803cbad19829d0b08e92bab89f29080ffd7 Mon Sep 17 00:00:00 2001 From: hedger Date: Fri, 28 Oct 2022 19:32:06 +0400 Subject: [PATCH 02/14] fbt: fixes for ufbt compat (#1940) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fbt: split sdk management code * scripts: fixed import handling * fbt: sdk: reformatted paths * scrips: dist: bundling libs as a build artifact * fbt: sdk: better path management * typo fix * fbt: sdk: minor path handling fixes * toolchain: fixed windows toolchain download Co-authored-by: あく --- firmware.scons | 17 -- scripts/fbt/sdk/__init__.py | 44 +++ scripts/fbt/{sdk.py => sdk/cache.py} | 280 +----------------- scripts/fbt/sdk/collector.py | 238 +++++++++++++++ scripts/fbt_tools/fbt_extapps.py | 2 +- scripts/fbt_tools/fbt_sdk.py | 49 ++- scripts/sconsdist.py | 66 +++-- .../toolchain/windows-toolchain-download.ps1 | 6 +- site_scons/extapps.scons | 2 +- site_scons/firmwareopts.scons | 20 +- 10 files changed, 383 insertions(+), 341 deletions(-) create mode 100644 scripts/fbt/sdk/__init__.py rename scripts/fbt/{sdk.py => sdk/cache.py} (52%) create mode 100644 scripts/fbt/sdk/collector.py diff --git a/firmware.scons b/firmware.scons index d501996b3..63a1aa3f7 100644 --- a/firmware.scons +++ b/firmware.scons @@ -178,23 +178,6 @@ sources.extend( ) ) - -fwenv.AppendUnique( - LINKFLAGS=[ - "-specs=nano.specs", - "-specs=nosys.specs", - "-Wl,--gc-sections", - "-Wl,--undefined=uxTopUsedPriority", - "-Wl,--wrap,_malloc_r", - "-Wl,--wrap,_free_r", - "-Wl,--wrap,_calloc_r", - "-Wl,--wrap,_realloc_r", - "-n", - "-Xlinker", - "-Map=${TARGET}.map", - ], -) - # Debug # print(fwenv.Dump()) diff --git a/scripts/fbt/sdk/__init__.py b/scripts/fbt/sdk/__init__.py new file mode 100644 index 000000000..27da5f7c8 --- /dev/null +++ b/scripts/fbt/sdk/__init__.py @@ -0,0 +1,44 @@ +from typing import Set, ClassVar +from dataclasses import dataclass, field + + +@dataclass(frozen=True) +class ApiEntryFunction: + name: str + returns: str + params: str + + csv_type: ClassVar[str] = "Function" + + def dictify(self): + return dict(name=self.name, type=self.returns, params=self.params) + + +@dataclass(frozen=True) +class ApiEntryVariable: + name: str + var_type: str + + csv_type: ClassVar[str] = "Variable" + + def dictify(self): + return dict(name=self.name, type=self.var_type, params=None) + + +@dataclass(frozen=True) +class ApiHeader: + name: str + + csv_type: ClassVar[str] = "Header" + + def dictify(self): + return dict(name=self.name, type=None, params=None) + + +@dataclass +class ApiEntries: + # These are sets, to avoid creating duplicates when we have multiple + # declarations with same signature + functions: Set[ApiEntryFunction] = field(default_factory=set) + variables: Set[ApiEntryVariable] = field(default_factory=set) + headers: Set[ApiHeader] = field(default_factory=set) diff --git a/scripts/fbt/sdk.py b/scripts/fbt/sdk/cache.py similarity index 52% rename from scripts/fbt/sdk.py rename to scripts/fbt/sdk/cache.py index 48f935de3..62d42798c 100644 --- a/scripts/fbt/sdk.py +++ b/scripts/fbt/sdk/cache.py @@ -4,284 +4,18 @@ import csv import operator from enum import Enum, auto -from typing import List, Set, ClassVar, Any -from dataclasses import dataclass, field +from typing import Set, ClassVar, Any +from dataclasses import dataclass from ansi.color import fg -from cxxheaderparser.parser import CxxParser - - -# 'Fixing' complaints about typedefs -CxxParser._fundamentals.discard("wchar_t") - -from cxxheaderparser.types import ( - EnumDecl, - Field, - ForwardDecl, - FriendDecl, - Function, - Method, - Typedef, - UsingAlias, - UsingDecl, - Variable, - Pointer, - Type, - PQName, - NameSpecifier, - FundamentalSpecifier, - Parameter, - Array, - Value, - Token, - FunctionType, +from . import ( + ApiEntries, + ApiEntryFunction, + ApiEntryVariable, + ApiHeader, ) -from cxxheaderparser.parserstate import ( - State, - EmptyBlockState, - ClassBlockState, - ExternBlockState, - NamespaceBlockState, -) - - -@dataclass(frozen=True) -class ApiEntryFunction: - name: str - returns: str - params: str - - csv_type: ClassVar[str] = "Function" - - def dictify(self): - return dict(name=self.name, type=self.returns, params=self.params) - - -@dataclass(frozen=True) -class ApiEntryVariable: - name: str - var_type: str - - csv_type: ClassVar[str] = "Variable" - - def dictify(self): - return dict(name=self.name, type=self.var_type, params=None) - - -@dataclass(frozen=True) -class ApiHeader: - name: str - - csv_type: ClassVar[str] = "Header" - - def dictify(self): - return dict(name=self.name, type=None, params=None) - - -@dataclass -class ApiEntries: - # These are sets, to avoid creating duplicates when we have multiple - # declarations with same signature - functions: Set[ApiEntryFunction] = field(default_factory=set) - variables: Set[ApiEntryVariable] = field(default_factory=set) - headers: Set[ApiHeader] = field(default_factory=set) - - -class SymbolManager: - def __init__(self): - self.api = ApiEntries() - self.name_hashes = set() - - # Calculate hash of name and raise exception if it already is in the set - def _name_check(self, name: str): - name_hash = gnu_sym_hash(name) - if name_hash in self.name_hashes: - raise Exception(f"Hash collision on {name}") - self.name_hashes.add(name_hash) - - def add_function(self, function_def: ApiEntryFunction): - if function_def in self.api.functions: - return - self._name_check(function_def.name) - self.api.functions.add(function_def) - - def add_variable(self, variable_def: ApiEntryVariable): - if variable_def in self.api.variables: - return - self._name_check(variable_def.name) - self.api.variables.add(variable_def) - - def add_header(self, header: str): - self.api.headers.add(ApiHeader(header)) - - -def gnu_sym_hash(name: str): - h = 0x1505 - for c in name: - h = (h << 5) + h + ord(c) - return str(hex(h))[-8:] - - -class SdkCollector: - def __init__(self): - self.symbol_manager = SymbolManager() - - def add_header_to_sdk(self, header: str): - self.symbol_manager.add_header(header) - - def process_source_file_for_sdk(self, file_path: str): - visitor = SdkCxxVisitor(self.symbol_manager) - with open(file_path, "rt") as f: - content = f.read() - parser = CxxParser(file_path, content, visitor, None) - parser.parse() - - def get_api(self): - return self.symbol_manager.api - - -def stringify_array_dimension(size_descr): - if not size_descr: - return "" - return stringify_descr(size_descr) - - -def stringify_array_descr(type_descr): - assert isinstance(type_descr, Array) - return ( - stringify_descr(type_descr.array_of), - stringify_array_dimension(type_descr.size), - ) - - -def stringify_descr(type_descr): - if isinstance(type_descr, (NameSpecifier, FundamentalSpecifier)): - return type_descr.name - elif isinstance(type_descr, PQName): - return "::".join(map(stringify_descr, type_descr.segments)) - elif isinstance(type_descr, Pointer): - # Hack - if isinstance(type_descr.ptr_to, FunctionType): - return stringify_descr(type_descr.ptr_to) - return f"{stringify_descr(type_descr.ptr_to)}*" - elif isinstance(type_descr, Type): - return ( - f"{'const ' if type_descr.const else ''}" - f"{'volatile ' if type_descr.volatile else ''}" - f"{stringify_descr(type_descr.typename)}" - ) - elif isinstance(type_descr, Parameter): - return stringify_descr(type_descr.type) - elif isinstance(type_descr, Array): - # Hack for 2d arrays - if isinstance(type_descr.array_of, Array): - argtype, dimension = stringify_array_descr(type_descr.array_of) - return ( - f"{argtype}[{stringify_array_dimension(type_descr.size)}][{dimension}]" - ) - return f"{stringify_descr(type_descr.array_of)}[{stringify_array_dimension(type_descr.size)}]" - elif isinstance(type_descr, Value): - return " ".join(map(stringify_descr, type_descr.tokens)) - elif isinstance(type_descr, FunctionType): - return f"{stringify_descr(type_descr.return_type)} (*)({', '.join(map(stringify_descr, type_descr.parameters))})" - elif isinstance(type_descr, Token): - return type_descr.value - elif type_descr is None: - return "" - else: - raise Exception("unsupported type_descr: %s" % type_descr) - - -class SdkCxxVisitor: - def __init__(self, symbol_manager: SymbolManager): - self.api = symbol_manager - - def on_variable(self, state: State, v: Variable) -> None: - if not v.extern: - return - - self.api.add_variable( - ApiEntryVariable( - stringify_descr(v.name), - stringify_descr(v.type), - ) - ) - - def on_function(self, state: State, fn: Function) -> None: - if fn.inline or fn.has_body: - return - - self.api.add_function( - ApiEntryFunction( - stringify_descr(fn.name), - stringify_descr(fn.return_type), - ", ".join(map(stringify_descr, fn.parameters)) - + (", ..." if fn.vararg else ""), - ) - ) - - def on_define(self, state: State, content: str) -> None: - pass - - def on_pragma(self, state: State, content: str) -> None: - pass - - def on_include(self, state: State, filename: str) -> None: - pass - - def on_empty_block_start(self, state: EmptyBlockState) -> None: - pass - - def on_empty_block_end(self, state: EmptyBlockState) -> None: - pass - - def on_extern_block_start(self, state: ExternBlockState) -> None: - pass - - def on_extern_block_end(self, state: ExternBlockState) -> None: - pass - - def on_namespace_start(self, state: NamespaceBlockState) -> None: - pass - - def on_namespace_end(self, state: NamespaceBlockState) -> None: - pass - - def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None: - pass - - def on_typedef(self, state: State, typedef: Typedef) -> None: - pass - - def on_using_namespace(self, state: State, namespace: List[str]) -> None: - pass - - def on_using_alias(self, state: State, using: UsingAlias) -> None: - pass - - def on_using_declaration(self, state: State, using: UsingDecl) -> None: - pass - - def on_enum(self, state: State, enum: EnumDecl) -> None: - pass - - def on_class_start(self, state: ClassBlockState) -> None: - pass - - def on_class_field(self, state: State, f: Field) -> None: - pass - - def on_class_method(self, state: ClassBlockState, method: Method) -> None: - pass - - def on_class_friend(self, state: ClassBlockState, friend: FriendDecl) -> None: - pass - - def on_class_end(self, state: ClassBlockState) -> None: - pass - @dataclass(frozen=True) class SdkVersion: diff --git a/scripts/fbt/sdk/collector.py b/scripts/fbt/sdk/collector.py new file mode 100644 index 000000000..578a8c7a6 --- /dev/null +++ b/scripts/fbt/sdk/collector.py @@ -0,0 +1,238 @@ +from typing import List + +from cxxheaderparser.parser import CxxParser +from . import ( + ApiEntries, + ApiEntryFunction, + ApiEntryVariable, + ApiHeader, +) + + +# 'Fixing' complaints about typedefs +CxxParser._fundamentals.discard("wchar_t") + +from cxxheaderparser.types import ( + EnumDecl, + Field, + ForwardDecl, + FriendDecl, + Function, + Method, + Typedef, + UsingAlias, + UsingDecl, + Variable, + Pointer, + Type, + PQName, + NameSpecifier, + FundamentalSpecifier, + Parameter, + Array, + Value, + Token, + FunctionType, +) + +from cxxheaderparser.parserstate import ( + State, + EmptyBlockState, + ClassBlockState, + ExternBlockState, + NamespaceBlockState, +) + + +class SymbolManager: + def __init__(self): + self.api = ApiEntries() + self.name_hashes = set() + + # Calculate hash of name and raise exception if it already is in the set + def _name_check(self, name: str): + name_hash = gnu_sym_hash(name) + if name_hash in self.name_hashes: + raise Exception(f"Hash collision on {name}") + self.name_hashes.add(name_hash) + + def add_function(self, function_def: ApiEntryFunction): + if function_def in self.api.functions: + return + self._name_check(function_def.name) + self.api.functions.add(function_def) + + def add_variable(self, variable_def: ApiEntryVariable): + if variable_def in self.api.variables: + return + self._name_check(variable_def.name) + self.api.variables.add(variable_def) + + def add_header(self, header: str): + self.api.headers.add(ApiHeader(header)) + + +def gnu_sym_hash(name: str): + h = 0x1505 + for c in name: + h = (h << 5) + h + ord(c) + return str(hex(h))[-8:] + + +class SdkCollector: + def __init__(self): + self.symbol_manager = SymbolManager() + + def add_header_to_sdk(self, header: str): + self.symbol_manager.add_header(header) + + def process_source_file_for_sdk(self, file_path: str): + visitor = SdkCxxVisitor(self.symbol_manager) + with open(file_path, "rt") as f: + content = f.read() + parser = CxxParser(file_path, content, visitor, None) + parser.parse() + + def get_api(self): + return self.symbol_manager.api + + +def stringify_array_dimension(size_descr): + if not size_descr: + return "" + return stringify_descr(size_descr) + + +def stringify_array_descr(type_descr): + assert isinstance(type_descr, Array) + return ( + stringify_descr(type_descr.array_of), + stringify_array_dimension(type_descr.size), + ) + + +def stringify_descr(type_descr): + if isinstance(type_descr, (NameSpecifier, FundamentalSpecifier)): + return type_descr.name + elif isinstance(type_descr, PQName): + return "::".join(map(stringify_descr, type_descr.segments)) + elif isinstance(type_descr, Pointer): + # Hack + if isinstance(type_descr.ptr_to, FunctionType): + return stringify_descr(type_descr.ptr_to) + return f"{stringify_descr(type_descr.ptr_to)}*" + elif isinstance(type_descr, Type): + return ( + f"{'const ' if type_descr.const else ''}" + f"{'volatile ' if type_descr.volatile else ''}" + f"{stringify_descr(type_descr.typename)}" + ) + elif isinstance(type_descr, Parameter): + return stringify_descr(type_descr.type) + elif isinstance(type_descr, Array): + # Hack for 2d arrays + if isinstance(type_descr.array_of, Array): + argtype, dimension = stringify_array_descr(type_descr.array_of) + return ( + f"{argtype}[{stringify_array_dimension(type_descr.size)}][{dimension}]" + ) + return f"{stringify_descr(type_descr.array_of)}[{stringify_array_dimension(type_descr.size)}]" + elif isinstance(type_descr, Value): + return " ".join(map(stringify_descr, type_descr.tokens)) + elif isinstance(type_descr, FunctionType): + return f"{stringify_descr(type_descr.return_type)} (*)({', '.join(map(stringify_descr, type_descr.parameters))})" + elif isinstance(type_descr, Token): + return type_descr.value + elif type_descr is None: + return "" + else: + raise Exception("unsupported type_descr: %s" % type_descr) + + +class SdkCxxVisitor: + def __init__(self, symbol_manager: SymbolManager): + self.api = symbol_manager + + def on_variable(self, state: State, v: Variable) -> None: + if not v.extern: + return + + self.api.add_variable( + ApiEntryVariable( + stringify_descr(v.name), + stringify_descr(v.type), + ) + ) + + def on_function(self, state: State, fn: Function) -> None: + if fn.inline or fn.has_body: + return + + self.api.add_function( + ApiEntryFunction( + stringify_descr(fn.name), + stringify_descr(fn.return_type), + ", ".join(map(stringify_descr, fn.parameters)) + + (", ..." if fn.vararg else ""), + ) + ) + + def on_define(self, state: State, content: str) -> None: + pass + + def on_pragma(self, state: State, content: str) -> None: + pass + + def on_include(self, state: State, filename: str) -> None: + pass + + def on_empty_block_start(self, state: EmptyBlockState) -> None: + pass + + def on_empty_block_end(self, state: EmptyBlockState) -> None: + pass + + def on_extern_block_start(self, state: ExternBlockState) -> None: + pass + + def on_extern_block_end(self, state: ExternBlockState) -> None: + pass + + def on_namespace_start(self, state: NamespaceBlockState) -> None: + pass + + def on_namespace_end(self, state: NamespaceBlockState) -> None: + pass + + def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None: + pass + + def on_typedef(self, state: State, typedef: Typedef) -> None: + pass + + def on_using_namespace(self, state: State, namespace: List[str]) -> None: + pass + + def on_using_alias(self, state: State, using: UsingAlias) -> None: + pass + + def on_using_declaration(self, state: State, using: UsingDecl) -> None: + pass + + def on_enum(self, state: State, enum: EnumDecl) -> None: + pass + + def on_class_start(self, state: ClassBlockState) -> None: + pass + + def on_class_field(self, state: State, f: Field) -> None: + pass + + def on_class_method(self, state: ClassBlockState, method: Method) -> None: + pass + + def on_class_friend(self, state: ClassBlockState, friend: FriendDecl) -> None: + pass + + def on_class_end(self, state: ClassBlockState) -> None: + pass diff --git a/scripts/fbt_tools/fbt_extapps.py b/scripts/fbt_tools/fbt_extapps.py index 5a5dab572..38c943cc5 100644 --- a/scripts/fbt_tools/fbt_extapps.py +++ b/scripts/fbt_tools/fbt_extapps.py @@ -8,7 +8,7 @@ import os import pathlib from fbt.elfmanifest import assemble_manifest_data from fbt.appmanifest import FlipperApplication, FlipperManifestException -from fbt.sdk import SdkCache +from fbt.sdk.cache import SdkCache import itertools from ansi.color import fg diff --git a/scripts/fbt_tools/fbt_sdk.py b/scripts/fbt_tools/fbt_sdk.py index 0b6e22de5..f1f55bdb8 100644 --- a/scripts/fbt_tools/fbt_sdk.py +++ b/scripts/fbt_tools/fbt_sdk.py @@ -4,7 +4,7 @@ from SCons.Action import Action from SCons.Errors import UserError # from SCons.Scanner import C -from SCons.Script import Mkdir, Copy, Delete, Entry +from SCons.Script import Entry from SCons.Util import LogicalLines import os.path @@ -12,7 +12,8 @@ import posixpath import pathlib import json -from fbt.sdk import SdkCollector, SdkCache +from fbt.sdk.collector import SdkCollector +from fbt.sdk.cache import SdkCache def ProcessSdkDepends(env, filename): @@ -49,15 +50,19 @@ def prebuild_sdk_create_origin_file(target, source, env): class SdkMeta: - def __init__(self, env): + def __init__(self, env, tree_builder: "SdkTreeBuilder"): self.env = env + self.treebuilder = tree_builder def save_to(self, json_manifest_path: str): meta_contents = { - "sdk_symbols": self.env["SDK_DEFINITION"].name, + "sdk_symbols": self.treebuilder.build_sdk_file_path( + self.env["SDK_DEFINITION"].path + ), "cc_args": self._wrap_scons_vars("$CCFLAGS $_CCCOMCOM"), "cpp_args": self._wrap_scons_vars("$CXXFLAGS $CCFLAGS $_CCCOMCOM"), "linker_args": self._wrap_scons_vars("$LINKFLAGS"), + "linker_script": self.env.subst("${LINKER_SCRIPT_PATH}"), } with open(json_manifest_path, "wt") as f: json.dump(meta_contents, f, indent=4) @@ -68,6 +73,8 @@ class SdkMeta: class SdkTreeBuilder: + SDK_DIR_SUBST = "SDK_ROOT_DIR" + def __init__(self, env, target, source) -> None: self.env = env self.target = target @@ -88,6 +95,8 @@ class SdkTreeBuilder: self.header_depends = list( filter(lambda fname: fname.endswith(".h"), depends.split()), ) + self.header_depends.append(self.env.subst("${LINKER_SCRIPT_PATH}")) + self.header_depends.append(self.env.subst("${SDK_DEFINITION}")) self.header_dirs = sorted( set(map(os.path.normpath, map(os.path.dirname, self.header_depends))) ) @@ -102,17 +111,33 @@ class SdkTreeBuilder: ) sdk_dirs = ", ".join(f"'{dir}'" for dir in self.header_dirs) - for dir in full_fw_paths: - if dir in sdk_dirs: - filtered_paths.append( - posixpath.normpath(posixpath.join(self.target_sdk_dir_name, dir)) - ) + filtered_paths.extend( + map( + self.build_sdk_file_path, + filter(lambda path: path in sdk_dirs, full_fw_paths), + ) + ) sdk_env = self.env.Clone() - sdk_env.Replace(CPPPATH=filtered_paths) - meta = SdkMeta(sdk_env) + sdk_env.Replace( + CPPPATH=filtered_paths, + LINKER_SCRIPT=self.env.subst("${APP_LINKER_SCRIPT}"), + ORIG_LINKER_SCRIPT_PATH=self.env["LINKER_SCRIPT_PATH"], + LINKER_SCRIPT_PATH=self.build_sdk_file_path("${ORIG_LINKER_SCRIPT_PATH}"), + ) + + meta = SdkMeta(sdk_env, self) meta.save_to(self.target[0].path) + def build_sdk_file_path(self, orig_path: str) -> str: + return posixpath.normpath( + posixpath.join( + self.SDK_DIR_SUBST, + self.target_sdk_dir_name, + orig_path, + ) + ).replace("\\", "/") + def emitter(self, target, source, env): target_folder = target[0] target = [target_folder.File("sdk.opts")] @@ -128,8 +153,6 @@ class SdkTreeBuilder: for sdkdir in dirs_to_create: os.makedirs(sdkdir, exist_ok=True) - shutil.copy2(self.env["SDK_DEFINITION"].path, self.sdk_root_dir.path) - for header in self.header_depends: shutil.copy2(header, self.sdk_deploy_dir.File(header).path) diff --git a/scripts/sconsdist.py b/scripts/sconsdist.py index 4c0427894..7636c87bb 100644 --- a/scripts/sconsdist.py +++ b/scripts/sconsdist.py @@ -48,48 +48,52 @@ class Main(App): ) self.parser_copy.set_defaults(func=self.copy) - def get_project_filename(self, project, filetype): + def get_project_file_name(self, project: ProjectDir, filetype: str) -> str: # Temporary fix project_name = project.project - if project_name == "firmware": - if filetype == "zip": - project_name = "sdk" - elif filetype != "elf": - project_name = "full" + if project_name == "firmware" and filetype != "elf": + project_name = "full" - return f"{self.DIST_FILE_PREFIX}{self.target}-{project_name}-{self.args.suffix}.{filetype}" + return self.get_dist_file_name(project_name, filetype) - def get_dist_filepath(self, filename): + def get_dist_file_name(self, dist_artifact_type: str, filetype: str) -> str: + return f"{self.DIST_FILE_PREFIX}{self.target}-{dist_artifact_type}-{self.args.suffix}.{filetype}" + + def get_dist_file_path(self, filename: str) -> str: return join(self.output_dir_path, filename) - def copy_single_project(self, project): + def copy_single_project(self, project: ProjectDir) -> None: obj_directory = join("build", project.dir) for filetype in ("elf", "bin", "dfu", "json"): if exists(src_file := join(obj_directory, f"{project.project}.{filetype}")): shutil.copyfile( src_file, - self.get_dist_filepath( - self.get_project_filename(project, filetype) + self.get_dist_file_path( + self.get_project_file_name(project, filetype) ), ) - if exists(sdk_folder := join(obj_directory, "sdk")): - with zipfile.ZipFile( - self.get_dist_filepath(self.get_project_filename(project, "zip")), - "w", - zipfile.ZIP_DEFLATED, - ) as zf: - for root, dirs, files in walk(sdk_folder): - for file in files: - zf.write( - join(root, file), - relpath( - join(root, file), - sdk_folder, - ), - ) + for foldertype in ("sdk", "lib"): + if exists(sdk_folder := join(obj_directory, foldertype)): + self.package_zip(foldertype, sdk_folder) - def copy(self): + def package_zip(self, foldertype, sdk_folder): + with zipfile.ZipFile( + self.get_dist_file_path(self.get_dist_file_name(foldertype, "zip")), + "w", + zipfile.ZIP_DEFLATED, + ) as zf: + for root, _, files in walk(sdk_folder): + for file in files: + zf.write( + join(root, file), + relpath( + join(root, file), + sdk_folder, + ), + ) + + def copy(self) -> int: self.projects = dict( map( lambda pd: (pd.project, pd), @@ -144,12 +148,12 @@ class Main(App): "-t", self.target, "--dfu", - self.get_dist_filepath( - self.get_project_filename(self.projects["firmware"], "dfu") + self.get_dist_file_path( + self.get_project_file_name(self.projects["firmware"], "dfu") ), "--stage", - self.get_dist_filepath( - self.get_project_filename(self.projects["updater"], "bin") + self.get_dist_file_path( + self.get_project_file_name(self.projects["updater"], "bin") ), ] if self.args.resources: diff --git a/scripts/toolchain/windows-toolchain-download.ps1 b/scripts/toolchain/windows-toolchain-download.ps1 index 370f1a14a..aaed89856 100644 --- a/scripts/toolchain/windows-toolchain-download.ps1 +++ b/scripts/toolchain/windows-toolchain-download.ps1 @@ -23,12 +23,12 @@ if (!(Test-Path -LiteralPath "$repo_root\toolchain")) { New-Item "$repo_root\toolchain" -ItemType Directory } -Write-Host -NoNewline "Unziping Windows toolchain.." +Write-Host -NoNewline "Extracting Windows toolchain.." Add-Type -Assembly "System.IO.Compression.Filesystem" -[System.IO.Compression.ZipFile]::ExtractToDirectory("$toolchain_zip", "$repo_root\") +[System.IO.Compression.ZipFile]::ExtractToDirectory("$repo_root\$toolchain_zip", "$repo_root\") Move-Item -Path "$repo_root\$toolchain_dir" -Destination "$repo_root\toolchain\x86_64-windows" Write-Host "done!" -Write-Host -NoNewline "Clearing temporary files.." +Write-Host -NoNewline "Cleaning up temporary files.." Remove-Item -LiteralPath "$repo_root\$toolchain_zip" -Force Write-Host "done!" diff --git a/site_scons/extapps.scons b/site_scons/extapps.scons index ee317be3b..90d228e58 100644 --- a/site_scons/extapps.scons +++ b/site_scons/extapps.scons @@ -21,7 +21,7 @@ appenv = ENV.Clone( ) appenv.Replace( - LINKER_SCRIPT="application_ext", + LINKER_SCRIPT=appenv.subst("$APP_LINKER_SCRIPT"), ) appenv.AppendUnique( diff --git a/site_scons/firmwareopts.scons b/site_scons/firmwareopts.scons index f04b55cdd..9f707b4d8 100644 --- a/site_scons/firmwareopts.scons +++ b/site_scons/firmwareopts.scons @@ -32,12 +32,27 @@ else: ], ) -ENV.Append( +ENV.AppendUnique( LINKFLAGS=[ - "-Tfirmware/targets/f${TARGET_HW}/${LINKER_SCRIPT}.ld", + "-specs=nano.specs", + "-specs=nosys.specs", + "-Wl,--gc-sections", + "-Wl,--undefined=uxTopUsedPriority", + "-Wl,--wrap,_malloc_r", + "-Wl,--wrap,_free_r", + "-Wl,--wrap,_calloc_r", + "-Wl,--wrap,_realloc_r", + "-n", + "-Xlinker", + "-Map=${TARGET}.map", + "-T${LINKER_SCRIPT_PATH}", ], ) +ENV.SetDefault( + LINKER_SCRIPT_PATH="firmware/targets/f${TARGET_HW}/${LINKER_SCRIPT}.ld", +) + if ENV["FIRMWARE_BUILD_CFG"] == "updater": ENV.Append( IMAGE_BASE_ADDRESS="0x20000000", @@ -47,4 +62,5 @@ else: ENV.Append( IMAGE_BASE_ADDRESS="0x8000000", LINKER_SCRIPT="stm32wb55xx_flash", + APP_LINKER_SCRIPT="application_ext", ) From 457b9ae2a93ae6eb1be4fa98c7178e1f2b082fb3 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:32:52 +0300 Subject: [PATCH 03/14] icons upgrade, keeloq fix, api bump --- applications/plugins/flipfrid/application.fam | 1 + applications/plugins/flipfrid/flipfrid.h | 2 + .../plugins/flipfrid/images/125_10px.png | Bin 0 -> 308 bytes .../plugins/metronome/application.fam | 1 + .../plugins/metronome/gui_extensions.c | 1 + .../plugins/metronome/images/ButtonUp_7x4.png | Bin 0 -> 102 bytes .../signal_generator/views/signal_gen_pwm.c | 2 +- firmware/targets/f7/api_symbols.csv | 186 +----------------- lib/subghz/protocols/keeloq.c | 2 + 9 files changed, 9 insertions(+), 186 deletions(-) create mode 100644 applications/plugins/flipfrid/images/125_10px.png create mode 100644 applications/plugins/metronome/images/ButtonUp_7x4.png diff --git a/applications/plugins/flipfrid/application.fam b/applications/plugins/flipfrid/application.fam index 4a09f1064..07649efca 100644 --- a/applications/plugins/flipfrid/application.fam +++ b/applications/plugins/flipfrid/application.fam @@ -9,4 +9,5 @@ App( order=15, fap_icon="rfid_10px.png", fap_category="Tools", + fap_icon_assets="images", ) diff --git a/applications/plugins/flipfrid/flipfrid.h b/applications/plugins/flipfrid/flipfrid.h index 248e2322e..4e3e7a37b 100644 --- a/applications/plugins/flipfrid/flipfrid.h +++ b/applications/plugins/flipfrid/flipfrid.h @@ -15,6 +15,8 @@ #include #include +#include + #include #include diff --git a/applications/plugins/flipfrid/images/125_10px.png b/applications/plugins/flipfrid/images/125_10px.png new file mode 100644 index 0000000000000000000000000000000000000000..ce01284a2c1f3eb413f581b84f1fb3f3a2a7223b GIT binary patch literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xkYHHq`AGmsv7|ftIx;Y9?C1WI$O_~uBzpw; zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpafHrx4R1i<>&pI=m5)bWZjP>yH&963)5S4_<9hOs!iI #include +#include //lib can only do bottom left/right void elements_button_top_left(Canvas* canvas, const char* str) { diff --git a/applications/plugins/metronome/images/ButtonUp_7x4.png b/applications/plugins/metronome/images/ButtonUp_7x4.png new file mode 100644 index 0000000000000000000000000000000000000000..1be79328b40a93297a5609756328406565c437c0 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)I!3HFqj;YoHDIHH2#}J8d-yTOk1_O>mFaFD) zeWb+ZHz{mGZZ1QpXe09^4tcYT#4oe=UbmGC^A-KE*|F&zP#=S*tDnm{r-UX30HgpM AM*si- literal 0 HcmV?d00001 diff --git a/applications/plugins/signal_generator/views/signal_gen_pwm.c b/applications/plugins/signal_generator/views/signal_gen_pwm.c index 8e618f8a9..a6f3de26d 100644 --- a/applications/plugins/signal_generator/views/signal_gen_pwm.c +++ b/applications/plugins/signal_generator/views/signal_gen_pwm.c @@ -1,7 +1,7 @@ #include "../signal_gen_app_i.h" #include "furi_hal.h" #include -#include +#include typedef enum { LineIndexChannel, diff --git a/firmware/targets/f7/api_symbols.csv b/firmware/targets/f7/api_symbols.csv index 5c76ee6f0..c2a970c6c 100644 --- a/firmware/targets/f7/api_symbols.csv +++ b/firmware/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,6.1,, +Version,+,8.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, @@ -4211,191 +4211,7 @@ Function,-,yn,double,"int, double" Function,-,ynf,float,"int, float" Variable,-,AHBPrescTable,const uint32_t[16], Variable,-,APBPrescTable,const uint32_t[8], -Variable,+,A_125khz_14,const Icon, -Variable,+,A_BadUsb_14,const Icon, -Variable,+,A_Clock_14,const Icon, -Variable,+,A_Debug_14,const Icon, -Variable,+,A_FileManager_14,const Icon, -Variable,+,A_GPIO_14,const Icon, -Variable,+,A_Infrared_14,const Icon, -Variable,+,A_Levelup1_128x64,const Icon, -Variable,+,A_Levelup2_128x64,const Icon, -Variable,+,A_Loading_24,const Icon, -Variable,+,A_NFC_14,const Icon, -Variable,+,A_Plugins_14,const Icon, -Variable,+,A_Round_loader_8x8,const Icon, -Variable,+,A_Settings_14,const Icon, -Variable,+,A_Sub1ghz_14,const Icon, -Variable,+,A_U2F_14,const Icon, -Variable,+,A_UniRFRemix_14,const Icon, -Variable,+,A_iButton_14,const Icon, Variable,-,ITM_RxBuffer,volatile int32_t, -Variable,+,I_125_10px,const Icon, -Variable,+,I_ActiveConnection_50x64,const Icon, -Variable,+,I_Apps_10px,const Icon, -Variable,+,I_Alert_9x8,const Icon, -Variable,+,I_ArrowC_1_36x36,const Icon, -Variable,+,I_ArrowDownEmpty_14x15,const Icon, -Variable,+,I_ArrowDownFilled_14x15,const Icon, -Variable,+,I_ArrowUpEmpty_14x15,const Icon, -Variable,+,I_ArrowUpFilled_14x15,const Icon, -Variable,+,I_Attention_5x8,const Icon, -Variable,+,I_Auth_62x31,const Icon, -Variable,+,I_BLE_Pairing_128x64,const Icon, -Variable,+,I_Background_128x11,const Icon, -Variable,+,I_BatteryBody_52x28,const Icon, -Variable,+,I_Battery_16x16,const Icon, -Variable,+,I_Battery_26x8,const Icon, -Variable,+,I_Ble_connected_15x15,const Icon, -Variable,+,I_Ble_disconnected_15x15,const Icon, -Variable,+,I_Bluetooth_Connected_16x8,const Icon, -Variable,+,I_Bluetooth_Idle_5x8,const Icon, -Variable,+,I_ButtonCenter_7x7,const Icon, -Variable,+,I_ButtonDown_7x4,const Icon, -Variable,+,I_ButtonLeftSmall_3x5,const Icon, -Variable,+,I_ButtonLeft_4x7,const Icon, -Variable,+,I_ButtonRightSmall_3x5,const Icon, -Variable,+,I_ButtonRight_4x7,const Icon, -Variable,+,I_ButtonUp_7x4,const Icon, -Variable,+,I_Button_18x18,const Icon, -Variable,+,I_Certification1_103x56,const Icon, -Variable,+,I_Certification2_98x33,const Icon, -Variable,+,I_Charging_lightning_9x10,const Icon, -Variable,+,I_Charging_lightning_mask_9x10,const Icon, -Variable,+,I_Circles_47x47,const Icon, -Variable,+,I_Clock_18x18,const Icon, -Variable,+,I_Connect_me_62x31,const Icon, -Variable,+,I_Connected_62x31,const Icon, -Variable,+,I_Cry_dolph_55x52,const Icon, -Variable,+,I_DFU_128x50,const Icon, -Variable,+,I_Detailed_chip_17x13,const Icon, -Variable,+,I_DolphinCommon_56x48,const Icon, -Variable,+,I_DolphinMafia_115x62,const Icon, -Variable,+,I_DolphinNice_96x59,const Icon, -Variable,+,I_DolphinReadingSuccess_59x63,const Icon, -Variable,+,I_DolphinWait_61x59,const Icon, -Variable,+,I_DoorLeft_70x55,const Icon, -Variable,+,I_DoorRight_70x55,const Icon, -Variable,+,I_Down_25x27,const Icon, -Variable,+,I_Down_hvr_25x27,const Icon, -Variable,+,I_Drive_112x35,const Icon, -Variable,+,I_Error_18x18,const Icon, -Variable,+,I_Error_62x31,const Icon, -Variable,+,I_EviSmile1_18x21,const Icon, -Variable,+,I_EviSmile2_18x21,const Icon, -Variable,+,I_EviWaiting1_18x21,const Icon, -Variable,+,I_EviWaiting2_18x21,const Icon, -Variable,+,I_FaceCharging_29x14,const Icon, -Variable,+,I_FaceConfused_29x14,const Icon, -Variable,+,I_FaceNopower_29x14,const Icon, -Variable,+,I_FaceNormal_29x14,const Icon, -Variable,+,I_GameMode_11x8,const Icon, -Variable,+,I_Health_16x16,const Icon, -Variable,+,I_Hidden_window_9x8,const Icon, -Variable,+,I_InfraredArrowDown_4x8,const Icon, -Variable,+,I_InfraredArrowUp_4x8,const Icon, -Variable,+,I_InfraredLearnShort_128x31,const Icon, -Variable,+,I_KeyBackspaceSelected_16x9,const Icon, -Variable,+,I_KeyBackspace_16x9,const Icon, -Variable,+,I_KeySaveSelected_24x11,const Icon, -Variable,+,I_KeySave_24x11,const Icon, -Variable,+,I_Keychain_39x36,const Icon, -Variable,+,I_Left_mouse_icon_9x9,const Icon, -Variable,+,I_Lock_7x8,const Icon, -Variable,+,I_Lock_8x8,const Icon, -Variable,+,I_MHz_25x11,const Icon, -Variable,+,I_Medium_chip_22x21,const Icon, -Variable,+,I_Mode_25x27,const Icon, -Variable,+,I_Mode_hvr_25x27,const Icon, -Variable,+,I_Modern_reader_18x34,const Icon, -Variable,+,I_Move_flipper_26x39,const Icon, -Variable,+,I_Mute_25x27,const Icon, -Variable,+,I_Mute_hvr_25x27,const Icon, -Variable,+,I_NFC_manual_60x50,const Icon, -Variable,+,I_Nfc_10px,const Icon, -Variable,+,I_Ok_btn_9x9,const Icon, -Variable,+,I_Ok_btn_pressed_13x13,const Icon, -Variable,+,I_Percent_10x14,const Icon, -Variable,+,I_Pin_arrow_down_7x9,const Icon, -Variable,+,I_Pin_arrow_left_9x7,const Icon, -Variable,+,I_Pin_arrow_right_9x7,const Icon, -Variable,+,I_Pin_arrow_up_7x9,const Icon, -Variable,+,I_Pin_attention_dpad_29x29,const Icon, -Variable,+,I_Pin_back_arrow_10x8,const Icon, -Variable,+,I_Pin_back_full_40x8,const Icon, -Variable,+,I_Pin_cell_13x13,const Icon, -Variable,+,I_Pin_pointer_5x3,const Icon, -Variable,+,I_Pin_star_7x7,const Icon, -Variable,+,I_Power_25x27,const Icon, -Variable,+,I_Power_hvr_25x27,const Icon, -Variable,+,I_Pressed_Button_13x13,const Icon, -Variable,+,I_Quest_7x8,const Icon, -Variable,+,I_RFIDDolphinReceive_97x61,const Icon, -Variable,+,I_RFIDDolphinSend_97x61,const Icon, -Variable,+,I_RFIDDolphinSuccess_108x57,const Icon, -Variable,+,I_RFIDSmallChip_14x14,const Icon, -Variable,+,I_Release_arrow_18x15,const Icon, -Variable,+,I_Restoring_38x32,const Icon, -Variable,+,I_Right_mouse_icon_9x9,const Icon, -Variable,+,I_Rotate_25x27,const Icon, -Variable,+,I_Rotate_hvr_25x27,const Icon, -Variable,+,I_SDQuestion_35x43,const Icon, -Variable,+,I_SDcardFail_11x8,const Icon, -Variable,+,I_SDcardMounted_11x8,const Icon, -Variable,+,I_Scanning_123x52,const Icon, -Variable,+,I_SmallArrowDown_3x5,const Icon, -Variable,+,I_SmallArrowDown_4x7,const Icon, -Variable,+,I_SmallArrowUp_3x5,const Icon, -Variable,+,I_SmallArrowUp_4x7,const Icon, -Variable,+,I_Smile_18x18,const Icon, -Variable,+,I_Space_65x18,const Icon, -Variable,+,I_Swing_25x27,const Icon, -Variable,+,I_Swing_hvr_25x27,const Icon, -Variable,+,I_Tap_reader_36x38,const Icon, -Variable,+,I_Temperature_16x16,const Icon, -Variable,+,I_Timer_25x27,const Icon, -Variable,+,I_Timer_hvr_25x27,const Icon, -Variable,+,I_Unlock_7x8,const Icon, -Variable,+,I_Unplug_bg_bottom_128x10,const Icon, -Variable,+,I_Unplug_bg_top_128x14,const Icon, -Variable,+,I_Up_25x27,const Icon, -Variable,+,I_Up_hvr_25x27,const Icon, -Variable,+,I_Updating_32x40,const Icon, -Variable,+,I_UsbTree_48x22,const Icon, -Variable,+,I_Vol_down_25x27,const Icon, -Variable,+,I_Vol_down_hvr_25x27,const Icon, -Variable,+,I_Vol_up_25x27,const Icon, -Variable,+,I_Vol_up_hvr_25x27,const Icon, -Variable,+,I_Voldwn_6x6,const Icon, -Variable,+,I_Voltage_16x16,const Icon, -Variable,+,I_Volup_8x6,const Icon, -Variable,+,I_WarningDolphin_45x42,const Icon, -Variable,+,I_Warning_30x23,const Icon, -Variable,+,I_back_10px,const Icon, -Variable,+,I_badusb_10px,const Icon, -Variable,+,I_dir_10px,const Icon, -Variable,+,I_iButtonDolphinVerySuccess_108x52,const Icon, -Variable,+,I_iButtonKey_49x44,const Icon, -Variable,+,I_ibutt_10px,const Icon, -Variable,+,I_ir_10px,const Icon, -Variable,+,I_keyboard_10px,const Icon, -Variable,+,I_loading_10px,const Icon, -Variable,+,I_music_10px,const Icon, -Variable,+,I_passport_bad1_46x49,const Icon, -Variable,+,I_passport_bad2_46x49,const Icon, -Variable,+,I_passport_bad3_46x49,const Icon, -Variable,+,I_passport_bottom_128x18,const Icon, -Variable,+,I_passport_happy1_46x49,const Icon, -Variable,+,I_passport_happy2_46x49,const Icon, -Variable,+,I_passport_happy3_46x49,const Icon, -Variable,+,I_passport_left_6x46,const Icon, -Variable,+,I_passport_okay1_46x49,const Icon, -Variable,+,I_passport_okay2_46x49,const Icon, -Variable,+,I_passport_okay3_46x49,const Icon, -Variable,+,I_sub1_10px,const Icon, -Variable,+,I_u2f_10px,const Icon, -Variable,+,I_unknown_10px,const Icon, -Variable,+,I_update_10px,const Icon, Variable,-,MSIRangeTable,const uint32_t[16], Variable,-,SmpsPrescalerTable,const uint32_t[4][6], Variable,+,SystemCoreClock,uint32_t, diff --git a/lib/subghz/protocols/keeloq.c b/lib/subghz/protocols/keeloq.c index 085a6d7ad..6887218d7 100644 --- a/lib/subghz/protocols/keeloq.c +++ b/lib/subghz/protocols/keeloq.c @@ -666,6 +666,7 @@ static uint8_t subghz_protocol_keeloq_check_remote_controller_selector( decrypt = subghz_protocol_keeloq_common_decrypt(hop, man); if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { *manufacture_name = furi_string_get_cstr(manufacture_code->name); + mfname = *manufacture_name; return 1; } break; @@ -675,6 +676,7 @@ static uint8_t subghz_protocol_keeloq_check_remote_controller_selector( decrypt = subghz_protocol_keeloq_common_decrypt(hop, man); if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { *manufacture_name = furi_string_get_cstr(manufacture_code->name); + mfname = *manufacture_name; return 1; } break; From 0a68d80028673555624507ead32b4f726604cb0b Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:34:35 +0300 Subject: [PATCH 04/14] update totp https://github.com/akopachov/flipper-zero_authenticator --- .../scenes/add_new_token/totp_input_text.c | 18 +- .../scenes/add_new_token/totp_input_text.h | 2 +- .../add_new_token/totp_scene_add_new_token.c | 15 +- .../add_new_token/totp_scene_add_new_token.h | 4 +- .../scenes/app_settings/totp_app_settings.c | 8 +- .../scenes/app_settings/totp_app_settings.h | 6 +- .../authenticate/totp_scene_authenticate.c | 7 +- .../authenticate/totp_scene_authenticate.h | 4 +- .../totp_scene_generate_token.c | 16 +- .../totp_scene_generate_token.h | 8 +- .../plugins/totp/scenes/scene_director.c | 10 +- .../plugins/totp/scenes/scene_director.h | 2 +- .../scenes/token_menu/totp_scene_token_menu.c | 17 +- .../scenes/token_menu/totp_scene_token_menu.h | 6 +- .../plugins/totp/scenes/totp_scenes_enum.h | 1 + applications/plugins/totp/services/cli/cli.c | 65 +++++ applications/plugins/totp/services/cli/cli.h | 7 + .../plugins/totp/services/cli/cli_helpers.c | 21 ++ .../plugins/totp/services/cli/cli_helpers.h | 40 ++++ .../totp/services/cli/commands/add/add.c | 226 ++++++++++++++++++ .../totp/services/cli/commands/add/add.h | 14 ++ .../services/cli/commands/delete/delete.c | 107 +++++++++ .../services/cli/commands/delete/delete.h | 13 + .../totp/services/cli/commands/help/help.c | 42 ++++ .../totp/services/cli/commands/help/help.h | 11 + .../totp/services/cli/commands/list/list.c | 71 ++++++ .../totp/services/cli/commands/list/list.h | 11 + .../services/cli/commands/timezone/timezone.c | 54 +++++ .../services/cli/commands/timezone/timezone.h | 12 + .../plugins/totp/services/config/config.c | 32 +-- .../plugins/totp/services/config/config.h | 4 +- .../plugins/totp/services/crypto/crypto.c | 19 +- .../plugins/totp/services/crypto/crypto.h | 10 +- .../plugins/totp/services/crypto/memset_s.c | 22 ++ .../plugins/totp/services/crypto/memset_s.h | 16 ++ .../plugins/totp/services/list/list.c | 4 + .../plugins/totp/services/totp/totp.c | 18 +- .../plugins/totp/services/totp/totp.h | 6 +- .../plugins/totp/services/ui/ui_controls.c | 6 +- .../plugins/totp/services/ui/ui_controls.h | 6 +- applications/plugins/totp/totp_app.c | 28 ++- .../plugins/totp/types/plugin_state.h | 2 +- applications/plugins/totp/types/token_info.c | 5 +- applications/plugins/totp/types/token_info.h | 4 +- 44 files changed, 886 insertions(+), 114 deletions(-) create mode 100644 applications/plugins/totp/services/cli/cli.c create mode 100644 applications/plugins/totp/services/cli/cli.h create mode 100644 applications/plugins/totp/services/cli/cli_helpers.c create mode 100644 applications/plugins/totp/services/cli/cli_helpers.h create mode 100644 applications/plugins/totp/services/cli/commands/add/add.c create mode 100644 applications/plugins/totp/services/cli/commands/add/add.h create mode 100644 applications/plugins/totp/services/cli/commands/delete/delete.c create mode 100644 applications/plugins/totp/services/cli/commands/delete/delete.h create mode 100644 applications/plugins/totp/services/cli/commands/help/help.c create mode 100644 applications/plugins/totp/services/cli/commands/help/help.h create mode 100644 applications/plugins/totp/services/cli/commands/list/list.c create mode 100644 applications/plugins/totp/services/cli/commands/list/list.h create mode 100644 applications/plugins/totp/services/cli/commands/timezone/timezone.c create mode 100644 applications/plugins/totp/services/cli/commands/timezone/timezone.h create mode 100644 applications/plugins/totp/services/crypto/memset_s.c create mode 100644 applications/plugins/totp/services/crypto/memset_s.h diff --git a/applications/plugins/totp/scenes/add_new_token/totp_input_text.c b/applications/plugins/totp/scenes/add_new_token/totp_input_text.c index e3a7f68a6..811cd2976 100644 --- a/applications/plugins/totp/scenes/add_new_token/totp_input_text.c +++ b/applications/plugins/totp/scenes/add_new_token/totp_input_text.c @@ -2,6 +2,16 @@ #include #include "../../types/common.h" +size_t strnlen(const char* s, size_t maxlen) { + size_t len; + + for(len = 0; len < maxlen; len++, s++) { + if(!*s) break; + } + + return len; +} + void view_draw(View* view, Canvas* canvas) { furi_assert(view); if(view->draw_callback) { @@ -32,10 +42,14 @@ static void commit_text_input_callback(void* context) { InputTextSceneState* text_input_state = (InputTextSceneState*)context; if(text_input_state->callback != 0) { InputTextSceneCallbackResult* result = malloc(sizeof(InputTextSceneCallbackResult)); - result->user_input_length = strlen(text_input_state->text_input_buffer); + result->user_input_length = + strnlen(text_input_state->text_input_buffer, INPUT_BUFFER_SIZE); result->user_input = malloc(result->user_input_length + 1); result->callback_data = text_input_state->callback_data; - strcpy(result->user_input, text_input_state->text_input_buffer); + strlcpy( + result->user_input, + text_input_state->text_input_buffer, + result->user_input_length + 1); text_input_state->callback(result); } } diff --git a/applications/plugins/totp/scenes/add_new_token/totp_input_text.h b/applications/plugins/totp/scenes/add_new_token/totp_input_text.h index a73a227b5..dda0dc301 100644 --- a/applications/plugins/totp/scenes/add_new_token/totp_input_text.h +++ b/applications/plugins/totp/scenes/add_new_token/totp_input_text.h @@ -10,7 +10,7 @@ typedef struct { char* user_input; - uint8_t user_input_length; + size_t user_input_length; void* callback_data; } InputTextSceneCallbackResult; diff --git a/applications/plugins/totp/scenes/add_new_token/totp_scene_add_new_token.c b/applications/plugins/totp/scenes/add_new_token/totp_scene_add_new_token.c index fc39b66cd..cc4e6a69d 100644 --- a/applications/plugins/totp/scenes/add_new_token/totp_scene_add_new_token.c +++ b/applications/plugins/totp/scenes/add_new_token/totp_scene_add_new_token.c @@ -25,9 +25,9 @@ typedef enum { typedef struct { char* token_name; - uint8_t token_name_length; + size_t token_name_length; char* token_secret; - uint8_t token_secret_length; + size_t token_secret_length; bool saved; Control selected_control; InputTextSceneContext* token_name_input_context; @@ -35,12 +35,12 @@ typedef struct { InputTextSceneState* input_state; uint32_t input_started_at; int16_t current_token_index; - int32_t screen_y_offset; + int16_t screen_y_offset; TokenHashAlgo algo; TokenDigitsCount digits_count; } SceneState; -void totp_scene_add_new_token_init(PluginState* plugin_state) { +void totp_scene_add_new_token_init(const PluginState* plugin_state) { UNUSED(plugin_state); } @@ -235,7 +235,10 @@ bool totp_scene_add_new_token_handle_event(PluginEvent* const event, PluginState if(token_secret_set) { tokenInfo->name = malloc(scene_state->token_name_length + 1); - strcpy(tokenInfo->name, scene_state->token_name); + strlcpy( + tokenInfo->name, + scene_state->token_name, + scene_state->token_name_length + 1); tokenInfo->algo = scene_state->algo; tokenInfo->digits = scene_state->digits_count; @@ -308,6 +311,6 @@ void totp_scene_add_new_token_deactivate(PluginState* plugin_state) { plugin_state->current_scene_state = NULL; } -void totp_scene_add_new_token_free(PluginState* plugin_state) { +void totp_scene_add_new_token_free(const PluginState* plugin_state) { UNUSED(plugin_state); } diff --git a/applications/plugins/totp/scenes/add_new_token/totp_scene_add_new_token.h b/applications/plugins/totp/scenes/add_new_token/totp_scene_add_new_token.h index b65c567a2..7a0b0e68a 100644 --- a/applications/plugins/totp/scenes/add_new_token/totp_scene_add_new_token.h +++ b/applications/plugins/totp/scenes/add_new_token/totp_scene_add_new_token.h @@ -10,11 +10,11 @@ typedef struct { uint8_t current_token_index; } TokenAddEditSceneContext; -void totp_scene_add_new_token_init(PluginState* plugin_state); +void totp_scene_add_new_token_init(const PluginState* plugin_state); void totp_scene_add_new_token_activate( PluginState* plugin_state, const TokenAddEditSceneContext* context); void totp_scene_add_new_token_render(Canvas* const canvas, PluginState* plugin_state); bool totp_scene_add_new_token_handle_event(PluginEvent* const event, PluginState* plugin_state); void totp_scene_add_new_token_deactivate(PluginState* plugin_state); -void totp_scene_add_new_token_free(PluginState* plugin_state); +void totp_scene_add_new_token_free(const PluginState* plugin_state); diff --git a/applications/plugins/totp/scenes/app_settings/totp_app_settings.c b/applications/plugins/totp/scenes/app_settings/totp_app_settings.c index cdb775a8d..4e03de433 100644 --- a/applications/plugins/totp/scenes/app_settings/totp_app_settings.c +++ b/applications/plugins/totp/scenes/app_settings/totp_app_settings.c @@ -16,7 +16,7 @@ typedef struct { Control selected_control; } SceneState; -void totp_scene_app_settings_init(PluginState* plugin_state) { +void totp_scene_app_settings_init(const PluginState* plugin_state) { UNUSED(plugin_state); } @@ -53,7 +53,7 @@ static void two_digit_to_str(int8_t num, char* str) { } void totp_scene_app_settings_render(Canvas* const canvas, PluginState* plugin_state) { - SceneState* scene_state = (SceneState*)plugin_state->current_scene_state; + const SceneState* scene_state = (SceneState*)plugin_state->current_scene_state; canvas_set_font(canvas, FontPrimary); canvas_draw_str_aligned(canvas, 0, 0, AlignLeft, AlignTop, "Timezone offset"); @@ -90,7 +90,7 @@ void totp_scene_app_settings_render(Canvas* const canvas, PluginState* plugin_st scene_state->selected_control == ConfirmButton); } -bool totp_scene_app_settings_handle_event(PluginEvent* const event, PluginState* plugin_state) { +bool totp_scene_app_settings_handle_event(const PluginEvent* const event, PluginState* plugin_state) { if(event->type == EventTypeKey) { SceneState* scene_state = (SceneState*)plugin_state->current_scene_state; if(event->input.type == InputTypePress) { @@ -171,6 +171,6 @@ void totp_scene_app_settings_deactivate(PluginState* plugin_state) { plugin_state->current_scene_state = NULL; } -void totp_scene_app_settings_free(PluginState* plugin_state) { +void totp_scene_app_settings_free(const PluginState* plugin_state) { UNUSED(plugin_state); } \ No newline at end of file diff --git a/applications/plugins/totp/scenes/app_settings/totp_app_settings.h b/applications/plugins/totp/scenes/app_settings/totp_app_settings.h index b97de3390..be51e9dc9 100644 --- a/applications/plugins/totp/scenes/app_settings/totp_app_settings.h +++ b/applications/plugins/totp/scenes/app_settings/totp_app_settings.h @@ -10,11 +10,11 @@ typedef struct { uint8_t current_token_index; } AppSettingsSceneContext; -void totp_scene_app_settings_init(PluginState* plugin_state); +void totp_scene_app_settings_init(const PluginState* plugin_state); void totp_scene_app_settings_activate( PluginState* plugin_state, const AppSettingsSceneContext* context); void totp_scene_app_settings_render(Canvas* const canvas, PluginState* plugin_state); -bool totp_scene_app_settings_handle_event(PluginEvent* const event, PluginState* plugin_state); +bool totp_scene_app_settings_handle_event(const PluginEvent* const event, PluginState* plugin_state); void totp_scene_app_settings_deactivate(PluginState* plugin_state); -void totp_scene_app_settings_free(PluginState* plugin_state); \ No newline at end of file +void totp_scene_app_settings_free(const PluginState* plugin_state); \ No newline at end of file diff --git a/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.c b/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.c index 73cbb4aaa..c03f87542 100644 --- a/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.c +++ b/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.c @@ -24,10 +24,11 @@ void totp_scene_authenticate_activate(PluginState* plugin_state) { scene_state->code_length = 0; memset(&scene_state->code_input[0], 0, MAX_CODE_LENGTH); plugin_state->current_scene_state = scene_state; + memset(&plugin_state->iv[0], 0, TOTP_IV_SIZE); } void totp_scene_authenticate_render(Canvas* const canvas, PluginState* plugin_state) { - SceneState* scene_state = (SceneState*)plugin_state->current_scene_state; + const SceneState* scene_state = (SceneState*)plugin_state->current_scene_state; int v_shift = 0; if(scene_state->code_length > 0) { @@ -72,7 +73,7 @@ void totp_scene_authenticate_render(Canvas* const canvas, PluginState* plugin_st } } -bool totp_scene_authenticate_handle_event(PluginEvent* const event, PluginState* plugin_state) { +bool totp_scene_authenticate_handle_event(const PluginEvent* const event, PluginState* plugin_state) { if(event->type == EventTypeKey) { if(event->input.type == InputTypeLong && event->input.key == InputKeyBack) { return false; @@ -155,6 +156,6 @@ void totp_scene_authenticate_deactivate(PluginState* plugin_state) { plugin_state->current_scene_state = NULL; } -void totp_scene_authenticate_free(PluginState* plugin_state) { +void totp_scene_authenticate_free(const PluginState* plugin_state) { UNUSED(plugin_state); } diff --git a/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.h b/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.h index f1199a425..aa308d983 100644 --- a/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.h +++ b/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.h @@ -9,6 +9,6 @@ void totp_scene_authenticate_init(PluginState* plugin_state); void totp_scene_authenticate_activate(PluginState* plugin_state); void totp_scene_authenticate_render(Canvas* const canvas, PluginState* plugin_state); -bool totp_scene_authenticate_handle_event(PluginEvent* const event, PluginState* plugin_state); +bool totp_scene_authenticate_handle_event(const PluginEvent* const event, PluginState* plugin_state); void totp_scene_authenticate_deactivate(PluginState* plugin_state); -void totp_scene_authenticate_free(PluginState* plugin_state); +void totp_scene_authenticate_free(const PluginState* plugin_state); diff --git a/applications/plugins/totp/scenes/generate_token/totp_scene_generate_token.c b/applications/plugins/totp/scenes/generate_token/totp_scene_generate_token.c index df5a3e746..7b5f7391b 100644 --- a/applications/plugins/totp/scenes/generate_token/totp_scene_generate_token.c +++ b/applications/plugins/totp/scenes/generate_token/totp_scene_generate_token.c @@ -9,6 +9,7 @@ #include "../../services/totp/totp.h" #include "../../services/config/config.h" #include "../../services/crypto/crypto.h" +#include "../../services/crypto/memset_s.h" #include "../scene_director.h" #include "../token_menu/totp_scene_token_menu.h" @@ -95,7 +96,7 @@ void update_totp_params(PluginState* const plugin_state) { } } -void totp_scene_generate_token_init(PluginState* plugin_state) { +void totp_scene_generate_token_init(const PluginState* plugin_state) { UNUSED(plugin_state); } @@ -130,7 +131,7 @@ void totp_scene_generate_token_activate( } } SceneState* scene_state = malloc(sizeof(SceneState)); - if(context == NULL) { + if(context == NULL || context->current_token_index > plugin_state->tokens_count) { scene_state->current_token_index = 0; } else { scene_state->current_token_index = context->current_token_index; @@ -180,7 +181,7 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_ ->data); if(tokenInfo->token != NULL && tokenInfo->token_length > 0) { - uint8_t key_length; + size_t key_length; uint8_t* key = totp_crypto_decrypt( tokenInfo->token, tokenInfo->token_length, &plugin_state->iv[0], &key_length); @@ -195,7 +196,7 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_ TOKEN_LIFETIME), scene_state->last_code, tokenInfo->digits); - memset(key, 0, key_length); + memset_s(key, sizeof(key), 0, key_length); free(key); } else { i_token_to_str(0, scene_state->last_code, tokenInfo->digits); @@ -265,7 +266,9 @@ void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_ } } -bool totp_scene_generate_token_handle_event(PluginEvent* const event, PluginState* plugin_state) { +bool totp_scene_generate_token_handle_event( + const PluginEvent* const event, + PluginState* plugin_state) { if(event->type == EventTypeKey) { if(event->input.type == InputTypeLong && event->input.key == InputKeyBack) { return false; @@ -314,11 +317,10 @@ void totp_scene_generate_token_deactivate(PluginState* plugin_state) { if(plugin_state->current_scene_state == NULL) return; SceneState* scene_state = (SceneState*)plugin_state->current_scene_state; - free(scene_state->last_code); free(scene_state); plugin_state->current_scene_state = NULL; } -void totp_scene_generate_token_free(PluginState* plugin_state) { +void totp_scene_generate_token_free(const PluginState* plugin_state) { UNUSED(plugin_state); } diff --git a/applications/plugins/totp/scenes/generate_token/totp_scene_generate_token.h b/applications/plugins/totp/scenes/generate_token/totp_scene_generate_token.h index 1284c7b41..e4ca818b6 100644 --- a/applications/plugins/totp/scenes/generate_token/totp_scene_generate_token.h +++ b/applications/plugins/totp/scenes/generate_token/totp_scene_generate_token.h @@ -10,11 +10,13 @@ typedef struct { uint8_t current_token_index; } GenerateTokenSceneContext; -void totp_scene_generate_token_init(PluginState* plugin_state); +void totp_scene_generate_token_init(const PluginState* plugin_state); void totp_scene_generate_token_activate( PluginState* plugin_state, const GenerateTokenSceneContext* context); void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_state); -bool totp_scene_generate_token_handle_event(PluginEvent* const event, PluginState* plugin_state); +bool totp_scene_generate_token_handle_event( + const PluginEvent* const event, + PluginState* plugin_state); void totp_scene_generate_token_deactivate(PluginState* plugin_state); -void totp_scene_generate_token_free(PluginState* plugin_state); +void totp_scene_generate_token_free(const PluginState* plugin_state); diff --git a/applications/plugins/totp/scenes/scene_director.c b/applications/plugins/totp/scenes/scene_director.c index 9a07b49fb..5265123f5 100644 --- a/applications/plugins/totp/scenes/scene_director.c +++ b/applications/plugins/totp/scenes/scene_director.c @@ -28,6 +28,8 @@ void totp_scene_director_activate_scene( case TotpSceneAppSettings: totp_scene_app_settings_activate(plugin_state, context); break; + case TotpSceneNone: + break; } plugin_state->current_scene = scene; @@ -51,6 +53,8 @@ void totp_scene_director_deactivate_active_scene(PluginState* const plugin_state case TotpSceneAppSettings: totp_scene_app_settings_deactivate(plugin_state); break; + case TotpSceneNone: + break; } } @@ -79,10 +83,12 @@ void totp_scene_director_render(Canvas* const canvas, PluginState* const plugin_ case TotpSceneAppSettings: totp_scene_app_settings_render(canvas, plugin_state); break; + case TotpSceneNone: + break; } } -void totp_scene_director_dispose(PluginState* const plugin_state) { +void totp_scene_director_dispose(const PluginState* const plugin_state) { totp_scene_generate_token_free(plugin_state); totp_scene_authenticate_free(plugin_state); totp_scene_add_new_token_free(plugin_state); @@ -108,6 +114,8 @@ bool totp_scene_director_handle_event(PluginEvent* const event, PluginState* con case TotpSceneAppSettings: processing = totp_scene_app_settings_handle_event(event, plugin_state); break; + case TotpSceneNone: + break; } return processing; diff --git a/applications/plugins/totp/scenes/scene_director.h b/applications/plugins/totp/scenes/scene_director.h index 3c25afff6..cc06029d3 100644 --- a/applications/plugins/totp/scenes/scene_director.h +++ b/applications/plugins/totp/scenes/scene_director.h @@ -12,5 +12,5 @@ void totp_scene_director_activate_scene( void totp_scene_director_deactivate_active_scene(PluginState* const plugin_state); void totp_scene_director_init_scenes(PluginState* const plugin_state); void totp_scene_director_render(Canvas* const canvas, PluginState* const plugin_state); -void totp_scene_director_dispose(PluginState* const plugin_state); +void totp_scene_director_dispose(const PluginState* const plugin_state); bool totp_scene_director_handle_event(PluginEvent* const event, PluginState* const plugin_state); diff --git a/applications/plugins/totp/scenes/token_menu/totp_scene_token_menu.c b/applications/plugins/totp/scenes/token_menu/totp_scene_token_menu.c index 8e2356f86..c4286ded9 100644 --- a/applications/plugins/totp/scenes/token_menu/totp_scene_token_menu.c +++ b/applications/plugins/totp/scenes/token_menu/totp_scene_token_menu.c @@ -21,7 +21,7 @@ typedef struct { int16_t current_token_index; } SceneState; -void totp_scene_token_menu_init(PluginState* plugin_state) { +void totp_scene_token_menu_init(const PluginState* plugin_state) { UNUSED(plugin_state); } @@ -38,7 +38,7 @@ void totp_scene_token_menu_activate( } void totp_scene_token_menu_render(Canvas* const canvas, PluginState* plugin_state) { - SceneState* scene_state = (SceneState*)plugin_state->current_scene_state; + const SceneState* scene_state = (SceneState*)plugin_state->current_scene_state; if(scene_state->current_token_index < 0) { ui_control_button_render( canvas, @@ -84,7 +84,7 @@ void totp_scene_token_menu_render(Canvas* const canvas, PluginState* plugin_stat } } -bool totp_scene_token_menu_handle_event(PluginEvent* const event, PluginState* plugin_state) { +bool totp_scene_token_menu_handle_event(const PluginEvent* const event, PluginState* plugin_state) { if(event->type == EventTypeKey) { SceneState* scene_state = (SceneState*)plugin_state->current_scene_state; if(event->input.type == InputTypePress) { @@ -139,13 +139,8 @@ bool totp_scene_token_menu_handle_event(PluginEvent* const event, PluginState* p dialog_message_show(plugin_state->dialogs, message); dialog_message_free(message); if(dialog_result == DialogMessageButtonRight) { - uint8_t i = 0; - - ListNode* list_node = plugin_state->tokens_list; - while(i < scene_state->current_token_index && list_node->next != NULL) { - list_node = list_node->next; - i++; - } + ListNode* list_node = list_element_at( + plugin_state->tokens_list, scene_state->current_token_index); TokenInfo* tokenInfo = list_node->data; token_info_free(tokenInfo); @@ -197,6 +192,6 @@ void totp_scene_token_menu_deactivate(PluginState* plugin_state) { plugin_state->current_scene_state = NULL; } -void totp_scene_token_menu_free(PluginState* plugin_state) { +void totp_scene_token_menu_free(const PluginState* plugin_state) { UNUSED(plugin_state); } diff --git a/applications/plugins/totp/scenes/token_menu/totp_scene_token_menu.h b/applications/plugins/totp/scenes/token_menu/totp_scene_token_menu.h index 0b117cb25..52e33e728 100644 --- a/applications/plugins/totp/scenes/token_menu/totp_scene_token_menu.h +++ b/applications/plugins/totp/scenes/token_menu/totp_scene_token_menu.h @@ -10,11 +10,11 @@ typedef struct { uint8_t current_token_index; } TokenMenuSceneContext; -void totp_scene_token_menu_init(PluginState* plugin_state); +void totp_scene_token_menu_init(const PluginState* plugin_state); void totp_scene_token_menu_activate( PluginState* plugin_state, const TokenMenuSceneContext* context); void totp_scene_token_menu_render(Canvas* const canvas, PluginState* plugin_state); -bool totp_scene_token_menu_handle_event(PluginEvent* const event, PluginState* plugin_state); +bool totp_scene_token_menu_handle_event(const PluginEvent* const event, PluginState* plugin_state); void totp_scene_token_menu_deactivate(PluginState* plugin_state); -void totp_scene_token_menu_free(PluginState* plugin_state); +void totp_scene_token_menu_free(const PluginState* plugin_state); diff --git a/applications/plugins/totp/scenes/totp_scenes_enum.h b/applications/plugins/totp/scenes/totp_scenes_enum.h index 72bf4d76e..c2b153a02 100644 --- a/applications/plugins/totp/scenes/totp_scenes_enum.h +++ b/applications/plugins/totp/scenes/totp_scenes_enum.h @@ -1,6 +1,7 @@ #pragma once typedef enum { + TotpSceneNone, TotpSceneAuthentication, TotpSceneGenerateToken, TotpSceneAddNewToken, diff --git a/applications/plugins/totp/services/cli/cli.c b/applications/plugins/totp/services/cli/cli.c new file mode 100644 index 000000000..76e58a02d --- /dev/null +++ b/applications/plugins/totp/services/cli/cli.c @@ -0,0 +1,65 @@ +// Original idea: https://github.com/br0ziliy + +#include "cli.h" +#include +#include "cli_helpers.h" +#include "commands/list/list.h" +#include "commands/add/add.h" +#include "commands/delete/delete.h" +#include "commands/timezone/timezone.h" +#include "commands/help/help.h" + +static void totp_cli_print_unknown_command(const FuriString* unknown_command) { + TOTP_CLI_PRINTF( + "Command \"%s\" is unknown. Use \"" TOTP_CLI_COMMAND_HELP + "\" command to get list of available commands.", + furi_string_get_cstr(unknown_command)); +} + +static void totp_cli_handler(Cli* cli, FuriString* args, void* context) { + PluginState* plugin_state = (PluginState*)context; + + FuriString* cmd = furi_string_alloc(); + + args_read_string_and_trim(args, cmd); + + if(furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_HELP) == 0 || + furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_HELP_ALT) == 0 || + furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_HELP_ALT2) == 0 || furi_string_empty(cmd)) { + totp_cli_command_help_handle(); + } else if( + furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_ADD) == 0 || + furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_ADD_ALT) == 0 || + furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_ADD_ALT2) == 0) { + totp_cli_command_add_handle(plugin_state, args, cli); + } else if( + furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_LIST) == 0 || + furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_LIST_ALT) == 0) { + totp_cli_command_list_handle(plugin_state, cli); + } else if( + furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_DELETE) == 0 || + furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_DELETE_ALT) == 0) { + totp_cli_command_delete_handle(plugin_state, args, cli); + } else if( + furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_TIMEZONE) == 0 || + furi_string_cmp_str(cmd, TOTP_CLI_COMMAND_TIMEZONE_ALT) == 0) { + totp_cli_command_timezone_handle(plugin_state, args, cli); + } else { + totp_cli_print_unknown_command(cmd); + } + + furi_string_free(cmd); +} + +void totp_cli_register_command_handler(PluginState* plugin_state) { + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command( + cli, TOTP_CLI_COMMAND_NAME, CliCommandFlagParallelSafe, totp_cli_handler, plugin_state); + furi_record_close(RECORD_CLI); +} + +void totp_cli_unregister_command_handler() { + Cli* cli = furi_record_open(RECORD_CLI); + cli_delete_command(cli, TOTP_CLI_COMMAND_NAME); + furi_record_close(RECORD_CLI); +} \ No newline at end of file diff --git a/applications/plugins/totp/services/cli/cli.h b/applications/plugins/totp/services/cli/cli.h new file mode 100644 index 000000000..5297dd61b --- /dev/null +++ b/applications/plugins/totp/services/cli/cli.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include "../../types/plugin_state.h" + +void totp_cli_register_command_handler(PluginState* plugin_state); +void totp_cli_unregister_command_handler(); \ No newline at end of file diff --git a/applications/plugins/totp/services/cli/cli_helpers.c b/applications/plugins/totp/services/cli/cli_helpers.c new file mode 100644 index 000000000..4a0b8b352 --- /dev/null +++ b/applications/plugins/totp/services/cli/cli_helpers.c @@ -0,0 +1,21 @@ +#include "cli_helpers.h" +#include + +bool totp_cli_ensure_authenticated(const PluginState* plugin_state, Cli* cli) { + if(plugin_state->current_scene == TotpSceneAuthentication) { + TOTP_CLI_PRINTF("Pleases enter PIN on your flipper device\r\n"); + + while(plugin_state->current_scene == TotpSceneAuthentication && + !cli_cmd_interrupt_received(cli)) { + furi_delay_ms(100); + } + + TOTP_CLI_DELETE_LAST_LINE(); + + if(plugin_state->current_scene == TotpSceneAuthentication) { + return false; + } + } + + return true; +} \ No newline at end of file diff --git a/applications/plugins/totp/services/cli/cli_helpers.h b/applications/plugins/totp/services/cli/cli_helpers.h new file mode 100644 index 000000000..9b19f926b --- /dev/null +++ b/applications/plugins/totp/services/cli/cli_helpers.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include "../../types/plugin_state.h" + +#define TOTP_CLI_COMMAND_NAME "totp" + +#define DOCOPT_ARGUMENT(arg) "<" arg ">" +#define DOCOPT_OPTIONAL(param) "[" param "]" +#define DOCOPT_REQUIRED(param) "(" param ")" +#define DOCOPT_OPTION(option, value) option " " value +#define DOCOPT_SWITCH(option) option +#define DOCOPT_OPTIONS "[options]" +#define DOCOPT_DEFAULT(val) "[default: " val "]" + +#define TOTP_CLI_PRINTF(format, ...) \ + do { \ + _Pragma(STRINGIFY(GCC diagnostic push)) \ + _Pragma(STRINGIFY(GCC diagnostic ignored "-Wdouble-promotion")) \ + printf(format, ##__VA_ARGS__); \ + _Pragma(STRINGIFY(GCC diagnostic pop)) \ + } while(false) + +#define TOTP_CLI_DELETE_LAST_LINE() \ + TOTP_CLI_PRINTF("\033[A\33[2K\r"); \ + fflush(stdout) + +#define TOTP_CLI_DELETE_CURRENT_LINE() \ + TOTP_CLI_PRINTF("\33[2K\r"); \ + fflush(stdout) + +#define TOTP_CLI_DELETE_LAST_CHAR() \ + TOTP_CLI_PRINTF("\b \b"); \ + fflush(stdout) + +#define TOTP_CLI_PRINT_INVALID_ARGUMENTS() \ + TOTP_CLI_PRINTF( \ + "Invalid command arguments. use \"help\" command to get list of available commands") + +bool totp_cli_ensure_authenticated(const PluginState* plugin_state, Cli* cli); diff --git a/applications/plugins/totp/services/cli/commands/add/add.c b/applications/plugins/totp/services/cli/commands/add/add.c new file mode 100644 index 000000000..c1ce42617 --- /dev/null +++ b/applications/plugins/totp/services/cli/commands/add/add.c @@ -0,0 +1,226 @@ +#include "add.h" +#include +#include +#include "../../../list/list.h" +#include "../../../../types/token_info.h" +#include "../../../config/config.h" +#include "../../cli_helpers.h" +#include "../../../../scenes/scene_director.h" + +#define TOTP_CLI_COMMAND_ADD_ARG_NAME "name" +#define TOTP_CLI_COMMAND_ADD_ARG_ALGO "algo" +#define TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX "-a" +#define TOTP_CLI_COMMAND_ADD_ARG_DIGITS "digits" +#define TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX "-d" +#define TOTP_CLI_COMMAND_ADD_ARG_UNSECURE_PREFIX "-u" + +static bool token_info_set_digits_from_str(TokenInfo* token_info, const FuriString* str) { + switch(furi_string_get_char(str, 0)) { + case '6': + token_info->digits = TOTP_6_DIGITS; + return true; + case '8': + token_info->digits = TOTP_8_DIGITS; + return true; + } + + return false; +} + +static bool token_info_set_algo_from_str(TokenInfo* token_info, const FuriString* str) { + if(furi_string_cmpi_str(str, TOTP_CONFIG_TOKEN_ALGO_SHA1_NAME) == 0) { + token_info->algo = SHA1; + return true; + } + + if(furi_string_cmpi_str(str, TOTP_CONFIG_TOKEN_ALGO_SHA256_NAME) == 0) { + token_info->algo = SHA256; + return true; + } + + if(furi_string_cmpi_str(str, TOTP_CONFIG_TOKEN_ALGO_SHA512_NAME) == 0) { + token_info->algo = SHA512; + return true; + } + + return false; +} + +void totp_cli_command_add_docopt_commands() { + TOTP_CLI_PRINTF(" " TOTP_CLI_COMMAND_ADD ", " TOTP_CLI_COMMAND_ADD_ALT + ", " TOTP_CLI_COMMAND_ADD_ALT2 " Add new token\r\n"); +} + +void totp_cli_command_add_docopt_usage() { + TOTP_CLI_PRINTF( + " " TOTP_CLI_COMMAND_NAME + " " DOCOPT_REQUIRED(TOTP_CLI_COMMAND_ADD " | " TOTP_CLI_COMMAND_ADD_ALT " | " TOTP_CLI_COMMAND_ADD_ALT2) " " DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ADD_ARG_NAME) " " DOCOPT_OPTIONAL( + DOCOPT_OPTION( + TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX, + DOCOPT_ARGUMENT( + TOTP_CLI_COMMAND_ADD_ARG_ALGO))) " " DOCOPT_OPTIONAL(DOCOPT_OPTION(TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX, DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ADD_ARG_DIGITS))) " " DOCOPT_OPTIONAL(DOCOPT_SWITCH(TOTP_CLI_COMMAND_ADD_ARG_UNSECURE_PREFIX)) "\r\n"); +} + +void totp_cli_command_add_docopt_arguments() { + TOTP_CLI_PRINTF(" " TOTP_CLI_COMMAND_ADD_ARG_NAME " Token name\r\n"); +} + +void totp_cli_command_add_docopt_options() { + TOTP_CLI_PRINTF(" " DOCOPT_OPTION( + TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX, + DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_ADD_ARG_ALGO)) " Token hashing algorithm.\r\n"); + TOTP_CLI_PRINTF( + " Could be one of: sha1, sha256, sha512 " DOCOPT_DEFAULT("sha1") "\r\n"); + TOTP_CLI_PRINTF(" " DOCOPT_OPTION( + TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX, + DOCOPT_ARGUMENT( + TOTP_CLI_COMMAND_ADD_ARG_DIGITS)) " Number of digits to generate, one of: 6, 8 " DOCOPT_DEFAULT("6") "\r\n"); + TOTP_CLI_PRINTF(" " DOCOPT_SWITCH( + TOTP_CLI_COMMAND_ADD_ARG_UNSECURE_PREFIX) " Show console user input as-is without masking\r\n"); +} + +static void furi_string_secure_free(FuriString* str) { + for(long i = furi_string_size(str) - 1; i >= 0; i--) { + furi_string_set_char(str, i, '\0'); + } + + furi_string_free(str); +} + +void totp_cli_command_add_handle(PluginState* plugin_state, FuriString* args, Cli* cli) { + FuriString* temp_str = furi_string_alloc(); + TokenInfo* token_info = token_info_alloc(); + + // Reading token name + if(!args_read_probably_quoted_string_and_trim(args, temp_str)) { + TOTP_CLI_PRINT_INVALID_ARGUMENTS(); + furi_string_free(temp_str); + token_info_free(token_info); + return; + } + + size_t temp_cstr_len = furi_string_size(temp_str); + token_info->name = malloc(temp_cstr_len + 1); + strlcpy(token_info->name, furi_string_get_cstr(temp_str), temp_cstr_len + 1); + + // Read optional arguments + bool mask_user_input = true; + while(args_read_string_and_trim(args, temp_str)) { + bool parsed = false; + if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX) == 0) { + if(!args_read_string_and_trim(args, temp_str)) { + TOTP_CLI_PRINTF("Missed value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX + "\"\r\n"); + } else if(!token_info_set_algo_from_str(token_info, temp_str)) { + TOTP_CLI_PRINTF( + "\"%s\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_ALGO_PREFIX + "\"\r\n", + furi_string_get_cstr(temp_str)); + } else { + parsed = true; + } + } else if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX) == 0) { + if(!args_read_string_and_trim(args, temp_str)) { + TOTP_CLI_PRINTF( + "Missed value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX + "\"\r\n"); + } else if(!token_info_set_digits_from_str(token_info, temp_str)) { + TOTP_CLI_PRINTF( + "\"%s\" is incorrect value for argument \"" TOTP_CLI_COMMAND_ADD_ARG_DIGITS_PREFIX + "\"\r\n", + furi_string_get_cstr(temp_str)); + } else { + parsed = true; + } + } else if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_ADD_ARG_UNSECURE_PREFIX) == 0) { + mask_user_input = false; + parsed = true; + } + + if(!parsed) { + TOTP_CLI_PRINT_INVALID_ARGUMENTS(); + furi_string_free(temp_str); + token_info_free(token_info); + return; + } + } + + // Reading token secret + furi_string_reset(temp_str); + TOTP_CLI_PRINTF("Enter token secret and confirm with [ENTER]\r\n"); + + uint8_t c; + while(cli_read(cli, &c, 1) == 1) { + if(c == CliSymbolAsciiEsc) { + // Some keys generating escape-sequences + // We need to ignore them as we case about alpha-numerics only + uint8_t c2; + cli_read_timeout(cli, &c2, 1, 0); + cli_read_timeout(cli, &c2, 1, 0); + } else if(c == CliSymbolAsciiETX) { + TOTP_CLI_DELETE_CURRENT_LINE(); + TOTP_CLI_PRINTF("Cancelled by user\r\n"); + furi_string_secure_free(temp_str); + token_info_free(token_info); + return; + } else if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + if(mask_user_input) { + putc('*', stdout); + } else { + putc(c, stdout); + } + fflush(stdout); + furi_string_push_back(temp_str, c); + } else if(c == CliSymbolAsciiBackspace || c == CliSymbolAsciiDel) { + size_t temp_str_size = furi_string_size(temp_str); + if(temp_str_size > 0) { + TOTP_CLI_DELETE_LAST_CHAR(); + furi_string_left(temp_str, temp_str_size - 1); + } + } else if(c == CliSymbolAsciiCR) { + cli_nl(); + break; + } + } + + TOTP_CLI_DELETE_LAST_LINE(); + + if(!totp_cli_ensure_authenticated(plugin_state, cli)) { + furi_string_secure_free(temp_str); + token_info_free(token_info); + return; + } + + if(!token_info_set_secret( + token_info, + furi_string_get_cstr(temp_str), + furi_string_size(temp_str), + plugin_state->iv)) { + TOTP_CLI_PRINTF("Token secret seems to be invalid and can not be parsed\r\n"); + furi_string_secure_free(temp_str); + token_info_free(token_info); + return; + } + + furi_string_secure_free(temp_str); + + bool load_generate_token_scene = false; + if(plugin_state->current_scene == TotpSceneGenerateToken) { + totp_scene_director_activate_scene(plugin_state, TotpSceneNone, NULL); + load_generate_token_scene = true; + } + + if(plugin_state->tokens_list == NULL) { + plugin_state->tokens_list = list_init_head(token_info); + } else { + list_add(plugin_state->tokens_list, token_info); + } + plugin_state->tokens_count++; + totp_config_file_save_new_token(token_info); + + if(load_generate_token_scene) { + totp_scene_director_activate_scene(plugin_state, TotpSceneGenerateToken, NULL); + } + + TOTP_CLI_PRINTF("Token \"%s\" has been successfully added\r\n", token_info->name); +} \ No newline at end of file diff --git a/applications/plugins/totp/services/cli/commands/add/add.h b/applications/plugins/totp/services/cli/commands/add/add.h new file mode 100644 index 000000000..7b1138e5b --- /dev/null +++ b/applications/plugins/totp/services/cli/commands/add/add.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include "../../../../types/plugin_state.h" + +#define TOTP_CLI_COMMAND_ADD "add" +#define TOTP_CLI_COMMAND_ADD_ALT "mk" +#define TOTP_CLI_COMMAND_ADD_ALT2 "new" + +void totp_cli_command_add_handle(PluginState* plugin_state, FuriString* args, Cli* cli); +void totp_cli_command_add_docopt_commands(); +void totp_cli_command_add_docopt_usage(); +void totp_cli_command_add_docopt_arguments(); +void totp_cli_command_add_docopt_options(); \ No newline at end of file diff --git a/applications/plugins/totp/services/cli/commands/delete/delete.c b/applications/plugins/totp/services/cli/commands/delete/delete.c new file mode 100644 index 000000000..bbeb6ec4d --- /dev/null +++ b/applications/plugins/totp/services/cli/commands/delete/delete.c @@ -0,0 +1,107 @@ +#include "delete.h" + +#include +#include +#include +#include "../../../list/list.h" +#include "../../../config/config.h" +#include "../../cli_helpers.h" +#include "../../../../scenes/scene_director.h" + +#define TOTP_CLI_COMMAND_DELETE_ARG_INDEX "index" +#define TOTP_CLI_COMMAND_DELETE_ARG_FORCE_SUFFIX "-f" + +void totp_cli_command_delete_docopt_commands() { + TOTP_CLI_PRINTF(" " TOTP_CLI_COMMAND_DELETE ", " TOTP_CLI_COMMAND_DELETE_ALT + " Delete existing token\r\n"); +} + +void totp_cli_command_delete_docopt_usage() { + TOTP_CLI_PRINTF( + " " TOTP_CLI_COMMAND_NAME + " " DOCOPT_REQUIRED(TOTP_CLI_COMMAND_DELETE " | " TOTP_CLI_COMMAND_DELETE_ALT) " " DOCOPT_ARGUMENT( + TOTP_CLI_COMMAND_DELETE_ARG_INDEX) " " DOCOPT_OPTIONAL(DOCOPT_SWITCH(TOTP_CLI_COMMAND_DELETE_ARG_FORCE_SUFFIX)) "\r\n"); +} + +void totp_cli_command_delete_docopt_arguments() { + TOTP_CLI_PRINTF(" " TOTP_CLI_COMMAND_DELETE_ARG_INDEX " Token index in the list\r\n"); +} + +void totp_cli_command_delete_docopt_options() { + TOTP_CLI_PRINTF(" " DOCOPT_SWITCH( + TOTP_CLI_COMMAND_DELETE_ARG_FORCE_SUFFIX) " Force command to do not ask user for interactive confirmation\r\n"); +} + +void totp_cli_command_delete_handle(PluginState* plugin_state, FuriString* args, Cli* cli) { + int token_number; + if(!args_read_int_and_trim(args, &token_number) || token_number <= 0 || + token_number > plugin_state->tokens_count) { + TOTP_CLI_PRINT_INVALID_ARGUMENTS(); + return; + } + + FuriString* temp_str = furi_string_alloc(); + bool confirm_needed = true; + if(args_read_string_and_trim(args, temp_str)) { + if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_DELETE_ARG_FORCE_SUFFIX) == 0) { + confirm_needed = false; + } else { + TOTP_CLI_PRINTF("Unknown argument \"%s\"\r\n", furi_string_get_cstr(temp_str)); + TOTP_CLI_PRINT_INVALID_ARGUMENTS(); + furi_string_free(temp_str); + return; + } + } + furi_string_free(temp_str); + + if(!totp_cli_ensure_authenticated(plugin_state, cli)) { + return; + } + + ListNode* list_node = list_element_at(plugin_state->tokens_list, token_number - 1); + + TokenInfo* token_info = list_node->data; + + bool confirmed = !confirm_needed; + if(confirm_needed) { + TOTP_CLI_PRINTF("WARNING!\r\n"); + TOTP_CLI_PRINTF( + "TOKEN \"%s\" WILL BE PERMANENTLY DELETED WITHOUT ABILITY TO RECOVER IT.\r\n", + token_info->name); + TOTP_CLI_PRINTF("Confirm? [y/n]\r\n"); + fflush(stdout); + char user_pick; + do { + user_pick = tolower(cli_getc(cli)); + } while(user_pick != 'y' && user_pick != 'n' && user_pick != CliSymbolAsciiCR && + user_pick != CliSymbolAsciiETX && user_pick != CliSymbolAsciiEsc); + + confirmed = user_pick == 'y' || user_pick == CliSymbolAsciiCR; + } + + if(confirmed) { + if(!totp_cli_ensure_authenticated(plugin_state, cli)) { + return; + } + + bool activate_generate_token_scene = false; + if(plugin_state->current_scene != TotpSceneAuthentication) { + totp_scene_director_activate_scene(plugin_state, TotpSceneNone, NULL); + activate_generate_token_scene = true; + } + + plugin_state->tokens_list = list_remove(plugin_state->tokens_list, list_node); + plugin_state->tokens_count--; + + totp_full_save_config_file(plugin_state); + + if(activate_generate_token_scene) { + totp_scene_director_activate_scene(plugin_state, TotpSceneGenerateToken, NULL); + } + + TOTP_CLI_PRINTF("Token \"%s\" has been successfully deleted\r\n", token_info->name); + token_info_free(token_info); + } else { + TOTP_CLI_PRINTF("User not confirmed\r\n"); + } +} \ No newline at end of file diff --git a/applications/plugins/totp/services/cli/commands/delete/delete.h b/applications/plugins/totp/services/cli/commands/delete/delete.h new file mode 100644 index 000000000..0b60932e3 --- /dev/null +++ b/applications/plugins/totp/services/cli/commands/delete/delete.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include "../../../../types/plugin_state.h" + +#define TOTP_CLI_COMMAND_DELETE "delete" +#define TOTP_CLI_COMMAND_DELETE_ALT "rm" + +void totp_cli_command_delete_handle(PluginState* plugin_state, FuriString* args, Cli* cli); +void totp_cli_command_delete_docopt_commands(); +void totp_cli_command_delete_docopt_usage(); +void totp_cli_command_delete_docopt_arguments(); +void totp_cli_command_delete_docopt_options(); \ No newline at end of file diff --git a/applications/plugins/totp/services/cli/commands/help/help.c b/applications/plugins/totp/services/cli/commands/help/help.c new file mode 100644 index 000000000..ab592fbba --- /dev/null +++ b/applications/plugins/totp/services/cli/commands/help/help.c @@ -0,0 +1,42 @@ +#include "help.h" +#include "../../cli_helpers.h" +#include "../add/add.h" +#include "../delete/delete.h" +#include "../list/list.h" +#include "../timezone/timezone.h" + +void totp_cli_command_help_docopt_commands() { + TOTP_CLI_PRINTF(" " TOTP_CLI_COMMAND_HELP ", " TOTP_CLI_COMMAND_HELP_ALT + ", " TOTP_CLI_COMMAND_HELP_ALT2 " Show command usage help\r\n"); +} + +void totp_cli_command_help_docopt_usage() { + TOTP_CLI_PRINTF(" " TOTP_CLI_COMMAND_NAME " " DOCOPT_REQUIRED( + TOTP_CLI_COMMAND_HELP " | " TOTP_CLI_COMMAND_HELP_ALT + " | " TOTP_CLI_COMMAND_HELP_ALT2) "\r\n"); +} + +void totp_cli_command_help_handle() { + TOTP_CLI_PRINTF("Usage:\r\n"); + totp_cli_command_help_docopt_usage(); + totp_cli_command_list_docopt_usage(); + totp_cli_command_add_docopt_usage(); + totp_cli_command_delete_docopt_usage(); + totp_cli_command_timezone_docopt_usage(); + cli_nl(); + TOTP_CLI_PRINTF("Commands:\r\n"); + totp_cli_command_help_docopt_commands(); + totp_cli_command_list_docopt_commands(); + totp_cli_command_add_docopt_commands(); + totp_cli_command_delete_docopt_commands(); + totp_cli_command_timezone_docopt_commands(); + cli_nl(); + TOTP_CLI_PRINTF("Arguments:\r\n"); + totp_cli_command_add_docopt_arguments(); + totp_cli_command_delete_docopt_arguments(); + totp_cli_command_timezone_docopt_arguments(); + cli_nl(); + TOTP_CLI_PRINTF("Options:\r\n"); + totp_cli_command_add_docopt_options(); + totp_cli_command_delete_docopt_options(); +} \ No newline at end of file diff --git a/applications/plugins/totp/services/cli/commands/help/help.h b/applications/plugins/totp/services/cli/commands/help/help.h new file mode 100644 index 000000000..4268b8bc5 --- /dev/null +++ b/applications/plugins/totp/services/cli/commands/help/help.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#define TOTP_CLI_COMMAND_HELP "help" +#define TOTP_CLI_COMMAND_HELP_ALT "h" +#define TOTP_CLI_COMMAND_HELP_ALT2 "?" + +void totp_cli_command_help_handle(); +void totp_cli_command_help_docopt_commands(); +void totp_cli_command_help_docopt_usage(); \ No newline at end of file diff --git a/applications/plugins/totp/services/cli/commands/list/list.c b/applications/plugins/totp/services/cli/commands/list/list.c new file mode 100644 index 000000000..61791084d --- /dev/null +++ b/applications/plugins/totp/services/cli/commands/list/list.c @@ -0,0 +1,71 @@ +#include "list.h" +#include +#include "../../../list/list.h" +#include "../../../../types/token_info.h" +#include "../../../config/constants.h" +#include "../../cli_helpers.h" + +static char* get_algo_as_cstr(TokenHashAlgo algo) { + switch(algo) { + case SHA1: + return TOTP_CONFIG_TOKEN_ALGO_SHA1_NAME; + case SHA256: + return TOTP_CONFIG_TOKEN_ALGO_SHA256_NAME; + case SHA512: + return TOTP_CONFIG_TOKEN_ALGO_SHA512_NAME; + } + + return "UNKNOWN"; +} + +static uint8_t get_digits_as_int(TokenDigitsCount digits) { + switch(digits) { + case TOTP_6_DIGITS: + return 6; + case TOTP_8_DIGITS: + return 8; + } + + return 6; +} + +void totp_cli_command_list_docopt_commands() { + TOTP_CLI_PRINTF(" " TOTP_CLI_COMMAND_LIST ", " TOTP_CLI_COMMAND_LIST_ALT + " List all available tokens\r\n"); +} + +void totp_cli_command_list_docopt_usage() { + TOTP_CLI_PRINTF(" " TOTP_CLI_COMMAND_NAME " " DOCOPT_REQUIRED( + TOTP_CLI_COMMAND_LIST " | " TOTP_CLI_COMMAND_LIST_ALT) "\r\n"); +} + +void totp_cli_command_list_handle(PluginState* plugin_state, Cli* cli) { + if(!totp_cli_ensure_authenticated(plugin_state, cli)) { + return; + } + + if(plugin_state->tokens_list == NULL) { + TOTP_CLI_PRINTF("There are no tokens"); + return; + } + + ListNode* node = plugin_state->tokens_list; + + TOTP_CLI_PRINTF("+-----+-----------------------------+--------+--------+\r\n"); + TOTP_CLI_PRINTF("| %-*s | %-*s | %-*s | %-s |\r\n", 3, "#", 27, "Name", 6, "Algo", "Digits"); + TOTP_CLI_PRINTF("+-----+-----------------------------+--------+--------+\r\n"); + uint16_t index = 1; + while(node != NULL) { + TokenInfo* token_info = (TokenInfo*)node->data; + token_info_get_digits_count(token_info); + TOTP_CLI_PRINTF( + "| %-3" PRIu16 " | %-27.27s | %-6s | %-6" PRIu8 " |\r\n", + index, + token_info->name, + get_algo_as_cstr(token_info->algo), + get_digits_as_int(token_info->digits)); + node = node->next; + index++; + } + TOTP_CLI_PRINTF("+-----+-----------------------------+--------+--------+\r\n"); +} \ No newline at end of file diff --git a/applications/plugins/totp/services/cli/commands/list/list.h b/applications/plugins/totp/services/cli/commands/list/list.h new file mode 100644 index 000000000..d8c3cb127 --- /dev/null +++ b/applications/plugins/totp/services/cli/commands/list/list.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include "../../../../types/plugin_state.h" + +#define TOTP_CLI_COMMAND_LIST "list" +#define TOTP_CLI_COMMAND_LIST_ALT "ls" + +void totp_cli_command_list_handle(PluginState* plugin_state, Cli* cli); +void totp_cli_command_list_docopt_commands(); +void totp_cli_command_list_docopt_usage(); diff --git a/applications/plugins/totp/services/cli/commands/timezone/timezone.c b/applications/plugins/totp/services/cli/commands/timezone/timezone.c new file mode 100644 index 000000000..d997aa116 --- /dev/null +++ b/applications/plugins/totp/services/cli/commands/timezone/timezone.c @@ -0,0 +1,54 @@ +#include "timezone.h" +#include +#include "../../../config/config.h" +#include "../../../../scenes/scene_director.h" +#include "../../cli_helpers.h" + +#define TOTP_CLI_COMMAND_TIMEZONE_ARG_TIMEZONE "timezone" + +void totp_cli_command_timezone_docopt_commands() { + TOTP_CLI_PRINTF(" " TOTP_CLI_COMMAND_TIMEZONE ", " TOTP_CLI_COMMAND_TIMEZONE_ALT + " Get or set current timezone\r\n"); +} + +void totp_cli_command_timezone_docopt_usage() { + TOTP_CLI_PRINTF( + " " TOTP_CLI_COMMAND_NAME + " " DOCOPT_REQUIRED(TOTP_CLI_COMMAND_TIMEZONE " | " TOTP_CLI_COMMAND_TIMEZONE_ALT) " " DOCOPT_OPTIONAL( + DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_TIMEZONE_ARG_TIMEZONE)) "\r\n"); +} + +void totp_cli_command_timezone_docopt_arguments() { + TOTP_CLI_PRINTF(" " TOTP_CLI_COMMAND_TIMEZONE_ARG_TIMEZONE + " Timezone offset in hours to be set.\r\n"); + TOTP_CLI_PRINTF( + " If not provided then current timezone offset will be printed\r\n"); +} + +void totp_cli_command_timezone_handle(PluginState* plugin_state, FuriString* args, Cli* cli) { + if(!totp_cli_ensure_authenticated(plugin_state, cli)) { + return; + } + + FuriString* temp_str = furi_string_alloc(); + if(args_read_string_and_trim(args, temp_str)) { + float tz = strtof(furi_string_get_cstr(temp_str), NULL); + if(tz >= -12.75f && tz <= 12.75f) { + plugin_state->timezone_offset = tz; + totp_config_file_update_timezone_offset(tz); + TOTP_CLI_PRINTF("Timezone is set to %f\r\n", tz); + if(plugin_state->current_scene == TotpSceneGenerateToken) { + totp_scene_director_activate_scene(plugin_state, TotpSceneNone, NULL); + totp_scene_director_activate_scene(plugin_state, TotpSceneGenerateToken, NULL); + } else if(plugin_state->current_scene == TotpSceneAppSettings) { + totp_scene_director_activate_scene(plugin_state, TotpSceneNone, NULL); + totp_scene_director_activate_scene(plugin_state, TotpSceneAppSettings, NULL); + } + } else { + TOTP_CLI_PRINTF("Invalid timezone offset\r\n"); + } + } else { + TOTP_CLI_PRINTF("Current timezone offset is %f\r\n", plugin_state->timezone_offset); + } + furi_string_free(temp_str); +} \ No newline at end of file diff --git a/applications/plugins/totp/services/cli/commands/timezone/timezone.h b/applications/plugins/totp/services/cli/commands/timezone/timezone.h new file mode 100644 index 000000000..0c0d82abf --- /dev/null +++ b/applications/plugins/totp/services/cli/commands/timezone/timezone.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include "../../../../types/plugin_state.h" + +#define TOTP_CLI_COMMAND_TIMEZONE "timezone" +#define TOTP_CLI_COMMAND_TIMEZONE_ALT "tz" + +void totp_cli_command_timezone_handle(PluginState* plugin_state, FuriString* args, Cli* cli); +void totp_cli_command_timezone_docopt_commands(); +void totp_cli_command_timezone_docopt_usage(); +void totp_cli_command_timezone_docopt_arguments(); \ No newline at end of file diff --git a/applications/plugins/totp/services/config/config.c b/applications/plugins/totp/services/config/config.c index 9a72f9ca8..bb6cffde0 100644 --- a/applications/plugins/totp/services/config/config.c +++ b/applications/plugins/totp/services/config/config.c @@ -10,7 +10,7 @@ #define CONFIG_FILE_PATH CONFIG_FILE_DIRECTORY_PATH "/totp.conf" #define CONFIG_FILE_BACKUP_PATH CONFIG_FILE_PATH ".backup" -uint8_t token_info_get_digits_as_int(TokenInfo* token_info) { +static uint8_t token_info_get_digits_as_int(const TokenInfo* token_info) { switch(token_info->digits) { case TOTP_6_DIGITS: return 6; @@ -21,7 +21,7 @@ uint8_t token_info_get_digits_as_int(TokenInfo* token_info) { return 6; } -void token_info_set_digits_from_int(TokenInfo* token_info, uint8_t digits) { +static void token_info_set_digits_from_int(TokenInfo* token_info, uint8_t digits) { switch(digits) { case 6: token_info->digits = TOTP_6_DIGITS; @@ -32,7 +32,7 @@ void token_info_set_digits_from_int(TokenInfo* token_info, uint8_t digits) { } } -char* token_info_get_algo_as_cstr(TokenInfo* token_info) { +static char* token_info_get_algo_as_cstr(const TokenInfo* token_info) { switch(token_info->algo) { case SHA1: return TOTP_CONFIG_TOKEN_ALGO_SHA1_NAME; @@ -45,12 +45,12 @@ char* token_info_get_algo_as_cstr(TokenInfo* token_info) { return NULL; } -void token_info_set_algo_from_str(TokenInfo* token_info, FuriString* str) { +static void token_info_set_algo_from_str(TokenInfo* token_info, const FuriString* str) { if(furi_string_cmpi_str(str, TOTP_CONFIG_TOKEN_ALGO_SHA1_NAME) == 0) { token_info->algo = SHA1; - } else if(furi_string_cmpi_str(str, TOTP_CONFIG_TOKEN_ALGO_SHA256_NAME)) { + } else if(furi_string_cmpi_str(str, TOTP_CONFIG_TOKEN_ALGO_SHA256_NAME) == 0) { token_info->algo = SHA256; - } else if(furi_string_cmpi_str(str, TOTP_CONFIG_TOKEN_ALGO_SHA512_NAME)) { + } else if(furi_string_cmpi_str(str, TOTP_CONFIG_TOKEN_ALGO_SHA512_NAME) == 0) { token_info->algo = SHA512; } } @@ -152,7 +152,7 @@ FlipperFormat* totp_open_config_file(Storage* storage) { return fff_data_file; } -void totp_config_file_save_new_token_i(FlipperFormat* file, TokenInfo* token_info) { +void totp_config_file_save_new_token_i(FlipperFormat* file, const TokenInfo* token_info) { flipper_format_seek_to_end(file); flipper_format_write_string_cstr(file, TOTP_CONFIG_KEY_TOKEN_NAME, token_info->name); bool token_is_valid = token_info->token != NULL && token_info->token_length > 0; @@ -170,7 +170,7 @@ void totp_config_file_save_new_token_i(FlipperFormat* file, TokenInfo* token_inf flipper_format_write_uint32(file, TOTP_CONFIG_KEY_TOKEN_DIGITS, &digits_count_as_uint32, 1); } -void totp_config_file_save_new_token(TokenInfo* token_info) { +void totp_config_file_save_new_token(const TokenInfo* token_info) { Storage* cfg_storage = totp_open_storage(); FlipperFormat* file = totp_open_config_file(cfg_storage); @@ -190,7 +190,7 @@ void totp_config_file_update_timezone_offset(float new_timezone_offset) { totp_close_storage(); } -void totp_full_save_config_file(PluginState* const plugin_state) { +void totp_full_save_config_file(const PluginState* const plugin_state) { Storage* storage = totp_open_storage(); FlipperFormat* fff_data_file = flipper_format_file_alloc(storage); @@ -209,7 +209,7 @@ void totp_full_save_config_file(PluginState* const plugin_state) { flipper_format_write_bool(fff_data_file, TOTP_CONFIG_KEY_PINSET, &plugin_state->pin_set, 1); ListNode* node = plugin_state->tokens_list; while(node != NULL) { - TokenInfo* token_info = node->data; + const TokenInfo* token_info = node->data; totp_config_file_save_new_token_i(fff_data_file, token_info); node = node->next; } @@ -343,9 +343,9 @@ TokenLoadingResult totp_config_file_load_tokens(PluginState* const plugin_state) TokenInfo* tokenInfo = token_info_alloc(); - const char* temp_cstr = furi_string_get_cstr(temp_str); - tokenInfo->name = (char*)malloc(strlen(temp_cstr) + 1); - strcpy(tokenInfo->name, temp_cstr); + size_t temp_cstr_len = furi_string_size(temp_str); + tokenInfo->name = (char*)malloc(temp_cstr_len + 1); + strlcpy(tokenInfo->name, furi_string_get_cstr(temp_str), temp_cstr_len + 1); uint32_t secret_bytes_count; if(!flipper_format_get_value_count( @@ -355,9 +355,11 @@ TokenLoadingResult totp_config_file_load_tokens(PluginState* const plugin_state) if(secret_bytes_count == 1) { // Plain secret key if(flipper_format_read_string(fff_data_file, TOTP_CONFIG_KEY_TOKEN_SECRET, temp_str)) { - temp_cstr = furi_string_get_cstr(temp_str); if(token_info_set_secret( - tokenInfo, temp_cstr, strlen(temp_cstr), &plugin_state->iv[0])) { + tokenInfo, + furi_string_get_cstr(temp_str), + furi_string_size(temp_str), + &plugin_state->iv[0])) { FURI_LOG_W(LOGGING_TAG, "Token \"%s\" has plain secret", tokenInfo->name); } else { tokenInfo->token = NULL; diff --git a/applications/plugins/totp/services/config/config.h b/applications/plugins/totp/services/config/config.h index 76cb40b2c..d452ad4b3 100644 --- a/applications/plugins/totp/services/config/config.h +++ b/applications/plugins/totp/services/config/config.h @@ -16,8 +16,8 @@ Storage* totp_open_storage(); void totp_close_storage(); FlipperFormat* totp_open_config_file(Storage* storage); void totp_close_config_file(FlipperFormat* file); -void totp_full_save_config_file(PluginState* const plugin_state); +void totp_full_save_config_file(const PluginState* const plugin_state); void totp_config_file_load_base(PluginState* const plugin_state); TokenLoadingResult totp_config_file_load_tokens(PluginState* const plugin_state); -void totp_config_file_save_new_token(TokenInfo* token_info); +void totp_config_file_save_new_token(const TokenInfo* token_info); void totp_config_file_update_timezone_offset(float new_timezone_offset); diff --git a/applications/plugins/totp/services/crypto/crypto.c b/applications/plugins/totp/services/crypto/crypto.c index ade2f9f49..79e41e6fd 100644 --- a/applications/plugins/totp/services/crypto/crypto.c +++ b/applications/plugins/totp/services/crypto/crypto.c @@ -3,6 +3,7 @@ #include #include "../config/config.h" #include "../../types/common.h" +#include "memset_s.h" #define CRYPTO_KEY_SLOT 2 #define CRYPTO_VERIFY_KEY "FFF_Crypto_pass" @@ -11,13 +12,13 @@ uint8_t* totp_crypto_encrypt( const uint8_t* plain_data, - const uint8_t plain_data_length, + const size_t plain_data_length, const uint8_t* iv, - uint8_t* encrypted_data_length) { + size_t* encrypted_data_length) { uint8_t* encrypted_data; size_t remain = plain_data_length % CRYPTO_ALIGNMENT_FACTOR; if(remain) { - uint8_t plain_data_aligned_length = plain_data_length - remain + CRYPTO_ALIGNMENT_FACTOR; + size_t plain_data_aligned_length = plain_data_length - remain + CRYPTO_ALIGNMENT_FACTOR; uint8_t* plain_data_aligned = malloc(plain_data_aligned_length); memset(plain_data_aligned, 0, plain_data_aligned_length); memcpy(plain_data_aligned, plain_data, plain_data_length); @@ -29,7 +30,7 @@ uint8_t* totp_crypto_encrypt( furi_hal_crypto_encrypt(plain_data_aligned, encrypted_data, plain_data_aligned_length); furi_hal_crypto_store_unload_key(CRYPTO_KEY_SLOT); - memset(plain_data_aligned, 0, plain_data_aligned_length); + memset_s(plain_data_aligned, sizeof(plain_data_aligned), 0, plain_data_aligned_length); free(plain_data_aligned); } else { encrypted_data = malloc(plain_data_length); @@ -45,9 +46,9 @@ uint8_t* totp_crypto_encrypt( uint8_t* totp_crypto_decrypt( const uint8_t* encrypted_data, - const uint8_t encrypted_data_length, + const size_t encrypted_data_length, const uint8_t* iv, - uint8_t* decrypted_data_length) { + size_t* decrypted_data_length) { *decrypted_data_length = encrypted_data_length; uint8_t* decrypted_data = malloc(*decrypted_data_length); furi_hal_crypto_store_load_key(CRYPTO_KEY_SLOT, iv); @@ -56,7 +57,7 @@ uint8_t* totp_crypto_decrypt( return decrypted_data; } -void totp_crypto_seed_iv(PluginState* plugin_state, uint8_t* pin, uint8_t pin_length) { +void totp_crypto_seed_iv(PluginState* plugin_state, const uint8_t* pin, uint8_t pin_length) { if(plugin_state->crypto_verify_data == NULL) { FURI_LOG_D(LOGGING_TAG, "Generating new IV"); furi_hal_random_fill_buf(&plugin_state->base_iv[0], TOTP_IV_SIZE); @@ -118,8 +119,8 @@ void totp_crypto_seed_iv(PluginState* plugin_state, uint8_t* pin, uint8_t pin_le } bool totp_crypto_verify_key(const PluginState* plugin_state) { - uint8_t decrypted_key_length; - uint8_t* decrypted_key = totp_crypto_decrypt( + size_t decrypted_key_length; + const uint8_t* decrypted_key = totp_crypto_decrypt( plugin_state->crypto_verify_data, plugin_state->crypto_verify_data_length, &plugin_state->iv[0], diff --git a/applications/plugins/totp/services/crypto/crypto.h b/applications/plugins/totp/services/crypto/crypto.h index 9fc319659..f0a28f798 100644 --- a/applications/plugins/totp/services/crypto/crypto.h +++ b/applications/plugins/totp/services/crypto/crypto.h @@ -4,13 +4,13 @@ uint8_t* totp_crypto_encrypt( const uint8_t* plain_data, - const uint8_t plain_data_length, + const size_t plain_data_length, const uint8_t* iv, - uint8_t* encrypted_data_length); + size_t* encrypted_data_length); uint8_t* totp_crypto_decrypt( const uint8_t* encrypted_data, - const uint8_t encrypted_data_length, + const size_t encrypted_data_length, const uint8_t* iv, - uint8_t* decrypted_data_length); -void totp_crypto_seed_iv(PluginState* plugin_state, uint8_t* pin, uint8_t pin_length); + size_t* decrypted_data_length); +void totp_crypto_seed_iv(PluginState* plugin_state, const uint8_t* pin, uint8_t pin_length); bool totp_crypto_verify_key(const PluginState* plugin_state); \ No newline at end of file diff --git a/applications/plugins/totp/services/crypto/memset_s.c b/applications/plugins/totp/services/crypto/memset_s.c new file mode 100644 index 000000000..81c285c0d --- /dev/null +++ b/applications/plugins/totp/services/crypto/memset_s.c @@ -0,0 +1,22 @@ +#include "memset_s.h" + +#define RSIZE_MAX 0x7fffffffffffffffUL + +errno_t memset_s(void* s, rsize_t smax, int c, rsize_t n) { + if(!s || smax > RSIZE_MAX) { + return EINVAL; + } + + errno_t violation_present = 0; + if(n > smax) { + n = smax; + violation_present = EINVAL; + } + + volatile unsigned char* v = s; + for(rsize_t i = 0u; i < n; ++i) { + *v++ = (unsigned char)c; + } + + return violation_present; +} \ No newline at end of file diff --git a/applications/plugins/totp/services/crypto/memset_s.h b/applications/plugins/totp/services/crypto/memset_s.h new file mode 100644 index 000000000..2889e23b2 --- /dev/null +++ b/applications/plugins/totp/services/crypto/memset_s.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +#ifndef _RSIZE_T_DECLARED +typedef uint64_t rsize_t; +#define _RSIZE_T_DECLARED +#endif +#ifndef _ERRNOT_DECLARED +typedef int16_t errno_t; +#define _ERRNOT_DECLARED +#endif + +errno_t memset_s(void* s, rsize_t smax, int c, rsize_t n); \ No newline at end of file diff --git a/applications/plugins/totp/services/list/list.c b/applications/plugins/totp/services/list/list.c index 77df1105a..3a3317980 100644 --- a/applications/plugins/totp/services/list/list.c +++ b/applications/plugins/totp/services/list/list.c @@ -44,6 +44,10 @@ ListNode* list_element_at(ListNode* head, uint16_t index) { } ListNode* list_remove(ListNode* head, ListNode* ep) { + if(head == NULL) { + return NULL; + } + if(head == ep) { ListNode* new_head = head->next; free(head); diff --git a/applications/plugins/totp/services/totp/totp.c b/applications/plugins/totp/services/totp/totp.c index 90e49367d..68531e171 100644 --- a/applications/plugins/totp/services/totp/totp.c +++ b/applications/plugins/totp/services/totp/totp.c @@ -42,14 +42,14 @@ uint32_t otp_generate( TOTP_ALGO algo, uint8_t digits, const uint8_t* plain_secret, - uint8_t plain_secret_length, + size_t plain_secret_length, uint64_t input) { uint8_t* hmac = malloc(64); memset(hmac, 0, 64); uint64_t input_swapped = swap_uint64(input); - int hmac_len = (*(algo))(plain_secret, plain_secret_length, (uint8_t*)&input_swapped, 8, hmac); + int hmac_len = (*algo)(plain_secret, plain_secret_length, (uint8_t*)&input_swapped, 8, hmac); if(hmac_len == 0) { free(hmac); return OTP_ERROR; @@ -80,7 +80,7 @@ uint32_t totp_at( TOTP_ALGO algo, uint8_t digits, const uint8_t* plain_secret, - uint8_t plain_secret_length, + size_t plain_secret_length, uint64_t for_time, float timezone, uint8_t interval) { @@ -96,9 +96,9 @@ uint32_t totp_at( static int totp_algo_sha1( const uint8_t* key, - uint8_t key_length, + size_t key_length, const uint8_t* input, - uint8_t input_length, + size_t input_length, uint8_t* output) { hmac_sha1(key, key_length, input, input_length, output); return HMAC_SHA1_RESULT_SIZE; @@ -106,9 +106,9 @@ static int totp_algo_sha1( static int totp_algo_sha256( const uint8_t* key, - uint8_t key_length, + size_t key_length, const uint8_t* input, - uint8_t input_length, + size_t input_length, uint8_t* output) { hmac_sha256(key, key_length, input, input_length, output); return HMAC_SHA256_RESULT_SIZE; @@ -116,9 +116,9 @@ static int totp_algo_sha256( static int totp_algo_sha512( const uint8_t* key, - uint8_t key_length, + size_t key_length, const uint8_t* input, - uint8_t input_length, + size_t input_length, uint8_t* output) { hmac_sha512(key, key_length, input, input_length, output); return HMAC_SHA512_RESULT_SIZE; diff --git a/applications/plugins/totp/services/totp/totp.h b/applications/plugins/totp/services/totp/totp.h index 31e70e01b..431ca11aa 100644 --- a/applications/plugins/totp/services/totp/totp.h +++ b/applications/plugins/totp/services/totp/totp.h @@ -17,9 +17,9 @@ */ typedef int (*TOTP_ALGO)( const uint8_t* key, - uint8_t key_length, + size_t key_length, const uint8_t* input, - uint8_t input_length, + size_t input_length, uint8_t* output); /* @@ -47,7 +47,7 @@ uint32_t totp_at( TOTP_ALGO algo, uint8_t digits, const uint8_t* plain_secret, - uint8_t plain_secret_length, + size_t plain_secret_length, uint64_t for_time, float timezone, uint8_t interval); diff --git a/applications/plugins/totp/services/ui/ui_controls.c b/applications/plugins/totp/services/ui/ui_controls.c index 7f6a4dd4d..8ed73498c 100644 --- a/applications/plugins/totp/services/ui/ui_controls.c +++ b/applications/plugins/totp/services/ui/ui_controls.c @@ -5,7 +5,7 @@ #define TEXT_BOX_HEIGHT 13 #define TEXT_BOX_MARGIN 4 -void ui_control_text_box_render(Canvas* const canvas, int8_t y, char* text, bool is_selected) { +void ui_control_text_box_render(Canvas* const canvas, int16_t y, const char* text, bool is_selected) { if(y < -TEXT_BOX_HEIGHT) { return; } @@ -44,7 +44,7 @@ void ui_control_select_render( int16_t x, int16_t y, uint8_t width, - char* text, + const char* text, bool is_selected) { if(y < -TEXT_BOX_HEIGHT) { return; @@ -99,7 +99,7 @@ void ui_control_button_render( int16_t y, uint8_t width, uint8_t height, - char* text, + const char* text, bool is_selected) { if(y < -height) { return; diff --git a/applications/plugins/totp/services/ui/ui_controls.h b/applications/plugins/totp/services/ui/ui_controls.h index e86b3e5d9..02b278cf9 100644 --- a/applications/plugins/totp/services/ui/ui_controls.h +++ b/applications/plugins/totp/services/ui/ui_controls.h @@ -3,19 +3,19 @@ #include #include -void ui_control_text_box_render(Canvas* const canvas, int8_t y, char* text, bool is_selected); +void ui_control_text_box_render(Canvas* const canvas, int16_t y, const char* text, bool is_selected); void ui_control_button_render( Canvas* const canvas, int16_t x, int16_t y, uint8_t width, uint8_t height, - char* text, + const char* text, bool is_selected); void ui_control_select_render( Canvas* const canvas, int16_t x, int16_t y, uint8_t width, - char* text, + const char* text, bool is_selected); diff --git a/applications/plugins/totp/totp_app.c b/applications/plugins/totp/totp_app.c index 24fcbd36d..3bc85e211 100644 --- a/applications/plugins/totp/totp_app.c +++ b/applications/plugins/totp/totp_app.c @@ -18,6 +18,7 @@ #include "scenes/scene_director.h" #include "services/ui/constants.h" #include "services/crypto/crypto.h" +#include "services/cli/cli.h" #define IDLE_TIMEOUT 60000 @@ -37,12 +38,15 @@ static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queu furi_message_queue_put(event_queue, &event, FuriWaitForever); } -static bool totp_state_init(PluginState* const plugin_state) { +static bool totp_plugin_state_init(PluginState* const plugin_state) { plugin_state->gui = furi_record_open(RECORD_GUI); plugin_state->notification = furi_record_open(RECORD_NOTIFICATION); plugin_state->dialogs = furi_record_open(RECORD_DIALOGS); + totp_config_file_load_base(plugin_state); + totp_cli_register_command_handler(plugin_state); + totp_scene_director_init_scenes(plugin_state); if (plugin_state->crypto_verify_data == NULL) { @@ -77,7 +81,9 @@ static bool totp_state_init(PluginState* const plugin_state) { return true; } -static void plugin_state_free(PluginState* plugin_state) { +static void totp_plugin_state_free(PluginState* plugin_state) { + totp_cli_unregister_command_handler(); + totp_scene_director_deactivate_active_scene(plugin_state); totp_scene_director_dispose(plugin_state); @@ -106,16 +112,16 @@ int32_t totp_app() { FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent)); PluginState* plugin_state = malloc(sizeof(PluginState)); - if (!totp_state_init(plugin_state)) { + if (!totp_plugin_state_init(plugin_state)) { FURI_LOG_E(LOGGING_TAG, "App state initialization failed\r\n"); - plugin_state_free(plugin_state); + totp_plugin_state_free(plugin_state); return 254; } ValueMutex state_mutex; if(!init_mutex(&state_mutex, plugin_state, sizeof(PluginState))) { FURI_LOG_E(LOGGING_TAG, "Cannot create mutex\r\n"); - plugin_state_free(plugin_state); + totp_plugin_state_free(plugin_state); return 255; } @@ -134,20 +140,20 @@ int32_t totp_app() { if (plugin_state->changing_scene) continue; FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100); - PluginState* plugin_state = acquire_mutex_block(&state_mutex); + PluginState* plugin_state_m = acquire_mutex_block(&state_mutex); if(event_status == FuriStatusOk) { if (event.type == EventTypeKey) { last_user_interaction_time = furi_get_tick(); } - processing = totp_scene_director_handle_event(&event, plugin_state); - } else if (plugin_state->pin_set && plugin_state->current_scene != TotpSceneAuthentication && furi_get_tick() - last_user_interaction_time > IDLE_TIMEOUT) { - totp_scene_director_activate_scene(plugin_state, TotpSceneAuthentication, NULL); + processing = totp_scene_director_handle_event(&event, plugin_state_m); + } else if (plugin_state_m->pin_set && plugin_state_m->current_scene != TotpSceneAuthentication && furi_get_tick() - last_user_interaction_time > IDLE_TIMEOUT) { + totp_scene_director_activate_scene(plugin_state_m, TotpSceneAuthentication, NULL); } view_port_update(view_port); - release_mutex(&state_mutex, plugin_state); + release_mutex(&state_mutex, plugin_state_m); } view_port_enabled_set(view_port, false); @@ -155,6 +161,6 @@ int32_t totp_app() { view_port_free(view_port); furi_message_queue_free(event_queue); delete_mutex(&state_mutex); - plugin_state_free(plugin_state); + totp_plugin_state_free(plugin_state); return 0; } diff --git a/applications/plugins/totp/types/plugin_state.h b/applications/plugins/totp/types/plugin_state.h index 98afe53cf..26e7e244b 100644 --- a/applications/plugins/totp/types/plugin_state.h +++ b/applications/plugins/totp/types/plugin_state.h @@ -22,7 +22,7 @@ typedef struct { uint8_t tokens_count; uint8_t* crypto_verify_data; - uint8_t crypto_verify_data_length; + size_t crypto_verify_data_length; bool pin_set; uint8_t iv[TOTP_IV_SIZE]; uint8_t base_iv[TOTP_IV_SIZE]; diff --git a/applications/plugins/totp/types/token_info.c b/applications/plugins/totp/types/token_info.c index 09ad1230a..2a37c4d44 100644 --- a/applications/plugins/totp/types/token_info.c +++ b/applications/plugins/totp/types/token_info.c @@ -5,6 +5,7 @@ #include "common.h" #include "../services/base32/base32.h" #include "../services/crypto/crypto.h" +#include "../services/crypto/memset_s.h" TokenInfo* token_info_alloc() { TokenInfo* tokenInfo = malloc(sizeof(TokenInfo)); @@ -23,7 +24,7 @@ void token_info_free(TokenInfo* token_info) { bool token_info_set_secret( TokenInfo* token_info, const char* base32_token_secret, - uint8_t token_secret_length, + size_t token_secret_length, uint8_t* iv) { uint8_t* plain_secret = malloc(token_secret_length); int plain_secret_length = @@ -37,7 +38,7 @@ bool token_info_set_secret( result = false; } - memset(plain_secret, 0, token_secret_length); + memset_s(plain_secret, sizeof(plain_secret), 0, token_secret_length); free(plain_secret); return result; } diff --git a/applications/plugins/totp/types/token_info.h b/applications/plugins/totp/types/token_info.h index e40c6e9bf..3d0f0ec61 100644 --- a/applications/plugins/totp/types/token_info.h +++ b/applications/plugins/totp/types/token_info.h @@ -8,7 +8,7 @@ typedef enum { TOTP_6_DIGITS, TOTP_8_DIGITS } TokenDigitsCount; typedef struct { uint8_t* token; - uint8_t token_length; + size_t token_length; char* name; TokenHashAlgo algo; TokenDigitsCount digits; @@ -19,6 +19,6 @@ void token_info_free(TokenInfo* token_info); bool token_info_set_secret( TokenInfo* token_info, const char* base32_token_secret, - uint8_t token_secret_length, + size_t token_secret_length, uint8_t* iv); uint8_t token_info_get_digits_count(TokenInfo* token_info); From 20e3a202aa07ffab12867d159c9d5062f03c1666 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:44:01 +0300 Subject: [PATCH 05/14] more icons --- applications/plugins/mousejacker/application.fam | 1 + .../plugins/mousejacker/images/badusb_10px.png | Bin 0 -> 576 bytes .../plugins/mousejacker/images/sub1_10px.png | Bin 0 -> 299 bytes applications/plugins/mousejacker/mousejacker.c | 1 + applications/plugins/picopass/picopass_i.h | 2 +- applications/plugins/totp/application.fam | 1 + .../plugins/totp/images/DolphinCommon_56x48.png | Bin 0 -> 1416 bytes applications/plugins/totp/services/ui/icons.h | 1 + applications/plugins/usbkeyboard/application.fam | 1 + .../plugins/usbkeyboard/assets/Arr_dwn_7x9.png | Bin 0 -> 3602 bytes .../plugins/usbkeyboard/assets/Arr_up_7x9.png | Bin 0 -> 3605 bytes .../usbkeyboard/assets/ButtonDown_7x4.png | Bin 0 -> 102 bytes .../usbkeyboard/assets/ButtonLeft_4x7.png | Bin 0 -> 1415 bytes .../usbkeyboard/assets/ButtonRight_4x7.png | Bin 0 -> 1839 bytes .../plugins/usbkeyboard/assets/ButtonUp_7x4.png | Bin 0 -> 102 bytes .../plugins/usbkeyboard/assets/Button_18x18.png | Bin 0 -> 3609 bytes .../plugins/usbkeyboard/assets/Circles_47x47.png | Bin 0 -> 3712 bytes .../usbkeyboard/assets/Left_mouse_icon_9x9.png | Bin 0 -> 3622 bytes .../plugins/usbkeyboard/assets/Like_def_11x9.png | Bin 0 -> 3616 bytes .../usbkeyboard/assets/Like_pressed_17x17.png | Bin 0 -> 3643 bytes .../plugins/usbkeyboard/assets/Ok_btn_9x9.png | Bin 0 -> 3605 bytes .../usbkeyboard/assets/Ok_btn_pressed_13x13.png | Bin 0 -> 3625 bytes .../usbkeyboard/assets/Pin_arrow_down_7x9.png | Bin 0 -> 3607 bytes .../usbkeyboard/assets/Pin_arrow_left_9x7.png | Bin 0 -> 3603 bytes .../usbkeyboard/assets/Pin_arrow_right_9x7.png | Bin 0 -> 3602 bytes .../usbkeyboard/assets/Pin_arrow_up_7x9.png | Bin 0 -> 3603 bytes .../usbkeyboard/assets/Pin_back_arrow_10x8.png | Bin 0 -> 3606 bytes .../usbkeyboard/assets/Pressed_Button_13x13.png | Bin 0 -> 3606 bytes .../usbkeyboard/assets/Right_mouse_icon_9x9.png | Bin 0 -> 3622 bytes .../plugins/usbkeyboard/assets/Space_65x18.png | Bin 0 -> 3619 bytes .../plugins/usbkeyboard/assets/Voldwn_6x6.png | Bin 0 -> 3593 bytes .../plugins/usbkeyboard/assets/Volup_8x6.png | Bin 0 -> 3595 bytes .../plugins/usbkeyboard/views/usb_hid_dirpad.c | 1 + .../plugins/usbkeyboard/views/usb_hid_keyboard.c | 1 + .../plugins/usbkeyboard/views/usb_hid_media.c | 1 + .../plugins/usbkeyboard/views/usb_hid_mouse.c | 1 + applications/plugins/wav_player/application.fam | 1 + .../plugins/wav_player/images/music_10px.png | Bin 0 -> 142 bytes applications/plugins/wav_player/wav_player.c | 2 ++ 39 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 applications/plugins/mousejacker/images/badusb_10px.png create mode 100644 applications/plugins/mousejacker/images/sub1_10px.png create mode 100644 applications/plugins/totp/images/DolphinCommon_56x48.png create mode 100644 applications/plugins/usbkeyboard/assets/Arr_dwn_7x9.png create mode 100644 applications/plugins/usbkeyboard/assets/Arr_up_7x9.png create mode 100644 applications/plugins/usbkeyboard/assets/ButtonDown_7x4.png create mode 100644 applications/plugins/usbkeyboard/assets/ButtonLeft_4x7.png create mode 100644 applications/plugins/usbkeyboard/assets/ButtonRight_4x7.png create mode 100644 applications/plugins/usbkeyboard/assets/ButtonUp_7x4.png create mode 100644 applications/plugins/usbkeyboard/assets/Button_18x18.png create mode 100644 applications/plugins/usbkeyboard/assets/Circles_47x47.png create mode 100644 applications/plugins/usbkeyboard/assets/Left_mouse_icon_9x9.png create mode 100644 applications/plugins/usbkeyboard/assets/Like_def_11x9.png create mode 100644 applications/plugins/usbkeyboard/assets/Like_pressed_17x17.png create mode 100644 applications/plugins/usbkeyboard/assets/Ok_btn_9x9.png create mode 100644 applications/plugins/usbkeyboard/assets/Ok_btn_pressed_13x13.png create mode 100644 applications/plugins/usbkeyboard/assets/Pin_arrow_down_7x9.png create mode 100644 applications/plugins/usbkeyboard/assets/Pin_arrow_left_9x7.png create mode 100644 applications/plugins/usbkeyboard/assets/Pin_arrow_right_9x7.png create mode 100644 applications/plugins/usbkeyboard/assets/Pin_arrow_up_7x9.png create mode 100644 applications/plugins/usbkeyboard/assets/Pin_back_arrow_10x8.png create mode 100644 applications/plugins/usbkeyboard/assets/Pressed_Button_13x13.png create mode 100644 applications/plugins/usbkeyboard/assets/Right_mouse_icon_9x9.png create mode 100644 applications/plugins/usbkeyboard/assets/Space_65x18.png create mode 100644 applications/plugins/usbkeyboard/assets/Voldwn_6x6.png create mode 100644 applications/plugins/usbkeyboard/assets/Volup_8x6.png create mode 100644 applications/plugins/wav_player/images/music_10px.png diff --git a/applications/plugins/mousejacker/application.fam b/applications/plugins/mousejacker/application.fam index 07ee51ae3..4a4093244 100644 --- a/applications/plugins/mousejacker/application.fam +++ b/applications/plugins/mousejacker/application.fam @@ -12,4 +12,5 @@ App( order=60, fap_icon="mouse_10px.png", fap_category="GPIO", + fap_icon_assets="images", ) diff --git a/applications/plugins/mousejacker/images/badusb_10px.png b/applications/plugins/mousejacker/images/badusb_10px.png new file mode 100644 index 0000000000000000000000000000000000000000..037474aa3bc9c2e1aca79a68483e69980432bcf5 GIT binary patch literal 576 zcmV-G0>AxEX>4Tx04R}tkv&MmKpe$i(`rSk4t5Z6$WWau6cusQDionYs1;guFuC*#nlvOW zE{=k0!NHHks)LKOt`4q(Aou~|=;Wm6A|?JWDYS_3;J6>}?mh0_0Yan9G%FATG`(u3 z5^*t;T@{0`5D-8=V(6BcWz0!Z5}xDh9zMR_MR}I@xj#prnzI<-6NzV;VOEJZh^IHJ z2Iqa^Fe}O`@j3ChNf#u3C`-Nm{=@yu+qV-Xlle$#1U1~DPPFA zta9Gstd(o5bx;1nP)=W2<~q$0B(R7jND!f*h7!uCB1)@HiiH&I$36VRj$a~|Laq`R zITlcX2HEk0|H1EWt^DMKn-q!zT`#u%F$x5Cfo9#dzmILZc>?&Kfh)c3uQY&}Ptxmc zEph}5Yy%h9ZB5w&E_Z;TCqp)6NAlAY@_FF>jJ_!g4Bi60Yi@6?eVjf3Y3eF@0~{Oz zV+G1y_jq?tXK(+WY4!I5C=YUpXXIhH00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<^lu`HWXkp{t5s906j@WK~xyijZi@f05Awj>HlAL zr$MwDdI>{Qf+U53tOUR#xOeyy)jcQo#JNRv)7r6DVVK|+*(cmT+R+EbO(O#X#REG4 O0000<>&pI=m5)cB{fFDGZlI8yr;B3<$MxhJ?+;A4eL&#) z0Ra}bue?07WhLz78x$BO|L3mq-MMxdP^D^#YeY#(Vo9o1a#1RfVlXl=GSoFN)ipE; zF*LF=Hn1|b&^9ozGB8+QQTzo(LvDUbW?CgwgE3G~h=Hk #include #include "mousejacker_ducky.h" +#include #define TAG "mousejacker" #define LOGITECH_MAX_CHANNEL 85 diff --git a/applications/plugins/picopass/picopass_i.h b/applications/plugins/picopass/picopass_i.h index 469a672b7..8f954c83c 100644 --- a/applications/plugins/picopass/picopass_i.h +++ b/applications/plugins/picopass/picopass_i.h @@ -24,7 +24,7 @@ #include #include -#include +#include #define PICOPASS_TEXT_STORE_SIZE 128 diff --git a/applications/plugins/totp/application.fam b/applications/plugins/totp/application.fam index 08f54619c..5b0266493 100644 --- a/applications/plugins/totp/application.fam +++ b/applications/plugins/totp/application.fam @@ -15,5 +15,6 @@ App( stack_size=2 * 1024, order=20, fap_category="Misc", + fap_icon_assets="images", fap_icon="totp_10px.png" ) diff --git a/applications/plugins/totp/images/DolphinCommon_56x48.png b/applications/plugins/totp/images/DolphinCommon_56x48.png new file mode 100644 index 0000000000000000000000000000000000000000..089aaed83507431993a76ca25d32fdd9664c1c84 GIT binary patch literal 1416 zcmaJ>eNYr-7(dh;KXS5&nWVIBjS_NizYg|x=Pr^vz*7zxJO|P-dw2IeZq?gec9-rD zoPZchQ_6}yP{Slc4I!!28K==nodOJ_nsCY-(wOq2uZbLx!rlYU{KIi)_Wj!D_j`WN z^FGgREXdEDF)ewT&1Re7Tj(uBvlG44lnH3;I%IzsO|z`*Vr!`uv?9QOwgs{#Ld+Ki zC9n_zxxBOkx@@+IwMwAaD)#3Ik`}gun2kLe))Crfb7e+#AgzHGCc+X$b>qJuIf`S7 z?8b}I{ghw#z>uiaLknQh@LJUrqHcVYS3v97F^OZN zCe|7^J|?QzUx0Zu17e(=CM1fYFpjtLk|a4~$g}e?hGH0!VoBOT&<=s(1ct%J9~?O} z$)jW_dkX9yTX~%W*i_IM%0{ z7EmP^_pKn`<5>E(SixgJU};7`)7Hidp&+DLnizsebUk}_-GfgbN^il9b`v)f+ z{o5Zry)d<7`fHQ^uw_;+x>mcPw0&8iW69x{k92O{Q}`yFdH=5d$pbf49w1&NS)G+vhr6y}5TMsofQirRDUmKilk5=(KGouJ{H9hW=$X zgi;)vI!jl!_4H3jD(?Jz=8By|i47I&tKA1y9{nfp;_|FxKBDNWp{hN9hJ1nU?z%J6 z?>UxyzWvO}Pgc~rCZ#5%Eq+_hNS~bBdiGlT&f%%e`hHjSySR2=JuK2^+%;$R3#Wz~ z=e_mfqW23bPa0fhe)HdE5+GelU&!jS3ckUZOQ)CC5?mo zo=tzG_4|RuvPUO|mhCwA>y)1c%SWC%a4?a-x|J*?ch~+n=R7o@>p6J2dE=$stKZmK z-xoTRwET2^Wu)&1U7!Ebw!!D?x`xwQX3pMnrRwCT?`4GHt4&?|cIiI{_^XYp-np>6 xE^lPSXzOYCC4X`6tl@OB1M5_S7jml-Y~(TPp{aTIejNKZ`m*!Atyxdk{0EAy49frj literal 0 HcmV?d00001 diff --git a/applications/plugins/totp/services/ui/icons.h b/applications/plugins/totp/services/ui/icons.h index a9139403f..2ce25a898 100644 --- a/applications/plugins/totp/services/ui/icons.h +++ b/applications/plugins/totp/services/ui/icons.h @@ -1,5 +1,6 @@ #pragma once +#include #include #define ICON_ARROW_LEFT_8x9_WIDTH 8 diff --git a/applications/plugins/usbkeyboard/application.fam b/applications/plugins/usbkeyboard/application.fam index b5ac0f74f..1e419f3fe 100644 --- a/applications/plugins/usbkeyboard/application.fam +++ b/applications/plugins/usbkeyboard/application.fam @@ -11,4 +11,5 @@ App( order=60, fap_icon="usb_keyboard_10px.png", fap_category="Misc", + fap_icon_assets="assets", ) diff --git a/applications/plugins/usbkeyboard/assets/Arr_dwn_7x9.png b/applications/plugins/usbkeyboard/assets/Arr_dwn_7x9.png new file mode 100644 index 0000000000000000000000000000000000000000..d4034efc432b102f6f751d001dc6891b0763c55c GIT binary patch literal 3602 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 JTZ;Aw|2NmVF(Lo} literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/Arr_up_7x9.png b/applications/plugins/usbkeyboard/assets/Arr_up_7x9.png new file mode 100644 index 0000000000000000000000000000000000000000..28b4236a292708b412629ffafe30f6d011491505 GIT binary patch literal 3605 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 AV*mgE literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/ButtonLeft_4x7.png b/applications/plugins/usbkeyboard/assets/ButtonLeft_4x7.png new file mode 100644 index 0000000000000000000000000000000000000000..0b4655d43247083aa705620e9836ac415b42ca46 GIT binary patch literal 1415 zcmbVM+iKK67*5rq)>aU2M7$VM1Vxif;vTv~W2u`S7ED{V3s&&L*<`XiG|9wd+THd> z5CnY!sdyuJtrvQyAo>KpiLcV|{Tkc)riAbluXfwSZCApL`ztB&p zx6LGKvks4K_4~)qD&oGa-YdJlW)hAKMNJd7<=t?6c^RI1>c$ifyjaM>^|&8!ey zB4!nh9u>5uen6Ve@<H5rru6h<2Ef#GQdQ*CmZOlQi~N!?9H`Rp;C% zU}CB21#?;r`&0|6C0}b-=jODa5|nEJ#ntxQ&{~jpgtwDta4hftr~G=#p@V36e4Zjh zq%J~{y26Jjn=1Nw-l*3%QW5YFE*v4z3gt0$&(*xf2en34c?JpH8+FYldo+Alvg8af-pG4(=!fyUi-Wsg z`g#n9VUcf(DFr{poMSNzw-lz>w+HV+n1ELr&SLA#LHUb0p(xWQ(1*vJ-i+1!`swxZ Z!O7;c$;lT_->m1Ovaz)0yuI`A$q$F8u*d)a literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/ButtonRight_4x7.png b/applications/plugins/usbkeyboard/assets/ButtonRight_4x7.png new file mode 100644 index 0000000000000000000000000000000000000000..8e1c74c1c0038ea55172f19ac875003fc80c2d06 GIT binary patch literal 1839 zcmcIlO>f*p7#Yw)M6zw!O+@VZ{?d|D~WYi~8rHRY?X-&T}Yen`g$^+EJ;z+|RV zE@PoDvZ9%#+_}3bC_5Cj8jDGq541mi{7F+&KF}W65sr$Xn5H|YrMQ2(J7%Yc%;(zO z57ax000=TsQ+1Ke@+w#iw3au3cGGQWY740k2ijH>P(6tD)S)be>gX6Tj7`<`b>di- zgWp$8Y+?i31~CzF0&E4uRlA=C(Mp~K`{74jEchB|)4DDK!ZVhSwdFyw0YIZ1cDh0S{OvfO-U_~ zvmRF*m9sWDXNH)GOyqS1Skhxbr6}s*7t&@~kFM(NW5}qh?Lu@lJ}HE;FDiLdGO>LO z5pS*%E2grR)l^;|?O5b_?u0me&c1U}%jrk8*%=Wk%i)8yp2P|kuxmKg<=(u_`oQRI_0 zS`-DNysBx=#3&qSkgA@hJP>~D+ZM(s5jI6Owp`?yE=3e`YGUqkVOp#Cp=3wR3O4hX zX6BLsN3UBzV(vI5;|SZHgOb=HD0VFjpTyfFW}GnQuh>2*Q`k>*cAmA#iUT7EXSpo# zkPm5~#I-o^cpgfe#P$=4-Pi*SpT!-@nJgp8L347xe>5EKl`=_ZFc8XGy+_j=_R_7! z@vZZMowS1GJ?Zw)eetks%~G{BTR>T}9|jt0j3Btyb*C3-`C?fwY3EY`q*oYZ39DpM z&uJ;PCZPLs4QO1Jd_|A1PF)azZJ)RZ`^-VMWr6e#XUOA%3eLG_Ch@BDOHzMk*MF0G zCo7xMd?Mg*HMIXw%nNz?%60fZiZPlqb?GqUpXO`F&Yi!okZl(n>P@r1P2i)yk3DgRwbHeNn6e|;J^SK4TM LH~i+q&mR8;k>NTA literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/ButtonUp_7x4.png b/applications/plugins/usbkeyboard/assets/ButtonUp_7x4.png new file mode 100644 index 0000000000000000000000000000000000000000..1be79328b40a93297a5609756328406565c437c0 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)I!3HFqj;YoHDIHH2#}J8d-yTOk1_O>mFaFD) zeWb+ZHz{mGZZ1QpXe09^4tcYT#4oe=UbmGC^A-KE*|F&zP#=S*tDnm{r-UX30HgpM AM*si- literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/Button_18x18.png b/applications/plugins/usbkeyboard/assets/Button_18x18.png new file mode 100644 index 0000000000000000000000000000000000000000..30a5b4fab236d8b57242559ef94fb1c5dbb5d10a GIT binary patch literal 3609 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 literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/Circles_47x47.png b/applications/plugins/usbkeyboard/assets/Circles_47x47.png new file mode 100644 index 0000000000000000000000000000000000000000..6a16ebf7bbe999fa143c683bef713a6e2f466cbb GIT binary patch literal 3712 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 biU<_Yn+9Io=xs3K`2!qm58ISjpSt)z2v?8| literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/Left_mouse_icon_9x9.png b/applications/plugins/usbkeyboard/assets/Left_mouse_icon_9x9.png new file mode 100644 index 0000000000000000000000000000000000000000..c533d85729f9b778cba33227141c90dea298f5e3 GIT binary patch literal 3622 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 literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/Like_def_11x9.png b/applications/plugins/usbkeyboard/assets/Like_def_11x9.png new file mode 100644 index 0000000000000000000000000000000000000000..555bea3d4b0239ae1be630d0306316d3e9494c4c GIT binary patch literal 3616 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 literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/Pin_arrow_down_7x9.png b/applications/plugins/usbkeyboard/assets/Pin_arrow_down_7x9.png new file mode 100644 index 0000000000000000000000000000000000000000..9687397afa81c92fb727bd40542830392fabdccb GIT binary patch literal 3607 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 QX&eA7&1_6djNPvM5BL~PlmGw# literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/Pin_arrow_left_9x7.png b/applications/plugins/usbkeyboard/assets/Pin_arrow_left_9x7.png new file mode 100644 index 0000000000000000000000000000000000000000..fb4ded78fde8d1bf4f053ba0b353e7acf14f031a GIT binary patch literal 3603 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 literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/Pin_arrow_up_7x9.png b/applications/plugins/usbkeyboard/assets/Pin_arrow_up_7x9.png new file mode 100644 index 0000000000000000000000000000000000000000..a91a6fd5e99a72112e28865cd8a004c7d1933fff GIT binary patch literal 3603 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 TFC+3}9cE|kU{!4CefEC<&8td2 literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/Pressed_Button_13x13.png b/applications/plugins/usbkeyboard/assets/Pressed_Button_13x13.png new file mode 100644 index 0000000000000000000000000000000000000000..823926b842b8f9868fd70d6f1434dff071c04d5d GIT binary patch literal 3606 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 literal 0 HcmV?d00001 diff --git a/applications/plugins/usbkeyboard/assets/Voldwn_6x6.png b/applications/plugins/usbkeyboard/assets/Voldwn_6x6.png new file mode 100644 index 0000000000000000000000000000000000000000..d7a82a2df8262667a9a03419f437ff9b350e645f GIT binary patch literal 3593 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 #include #include +#include struct UsbHidDirpad { View* view; diff --git a/applications/plugins/usbkeyboard/views/usb_hid_keyboard.c b/applications/plugins/usbkeyboard/views/usb_hid_keyboard.c index 687189e7d..71438e1c9 100644 --- a/applications/plugins/usbkeyboard/views/usb_hid_keyboard.c +++ b/applications/plugins/usbkeyboard/views/usb_hid_keyboard.c @@ -3,6 +3,7 @@ #include #include #include +#include struct UsbHidKeyboard { View* view; diff --git a/applications/plugins/usbkeyboard/views/usb_hid_media.c b/applications/plugins/usbkeyboard/views/usb_hid_media.c index 520d04717..8d2188434 100644 --- a/applications/plugins/usbkeyboard/views/usb_hid_media.c +++ b/applications/plugins/usbkeyboard/views/usb_hid_media.c @@ -2,6 +2,7 @@ #include #include #include +#include struct UsbHidMedia { View* view; diff --git a/applications/plugins/usbkeyboard/views/usb_hid_mouse.c b/applications/plugins/usbkeyboard/views/usb_hid_mouse.c index 77067ac04..27f2ac105 100644 --- a/applications/plugins/usbkeyboard/views/usb_hid_mouse.c +++ b/applications/plugins/usbkeyboard/views/usb_hid_mouse.c @@ -2,6 +2,7 @@ #include #include #include +#include struct UsbHidMouse { View* view; diff --git a/applications/plugins/wav_player/application.fam b/applications/plugins/wav_player/application.fam index 394e45d29..4040ed159 100644 --- a/applications/plugins/wav_player/application.fam +++ b/applications/plugins/wav_player/application.fam @@ -8,4 +8,5 @@ App( order=46, fap_icon="wav_10px.png", fap_category="Music", + fap_icon_assets="images", ) diff --git a/applications/plugins/wav_player/images/music_10px.png b/applications/plugins/wav_player/images/music_10px.png new file mode 100644 index 0000000000000000000000000000000000000000..d41eb0db8c822c60be6c097393b3682680b81a6c GIT binary patch literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVhivIasA%~WHIdey2}7aaTa() z7Bet#3xhBt!>l_x9Asc&(0a_=e)W&n8K5!-Pgg&ebxsLQ0Ao%f>i_@% literal 0 HcmV?d00001 diff --git a/applications/plugins/wav_player/wav_player.c b/applications/plugins/wav_player/wav_player.c index 830e37dbf..8798b788a 100644 --- a/applications/plugins/wav_player/wav_player.c +++ b/applications/plugins/wav_player/wav_player.c @@ -12,6 +12,8 @@ #include "wav_player_view.h" #include +#include + #define TAG "WavPlayer" #define WAVPLAYER_FOLDER "/ext/wav_player" From 09b622d4ae01ef6d3ffcfd0eefabfa5cf890ffdd Mon Sep 17 00:00:00 2001 From: Konstantin Volkov <72250702+doomwastaken@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:45:22 +0300 Subject: [PATCH 06/14] UnitTests: removed all continue-on-error lines (#1946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * removed all continue-on-error lines * Github: add assets deployment after format Co-authored-by: Konstantin Volkov Co-authored-by: あく --- .github/workflows/unit_tests.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 8c5bac2a2..b5bf10004 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -24,33 +24,29 @@ jobs: - name: 'Compile unit tests firmware' id: compile - continue-on-error: true run: | FBT_TOOLCHAIN_PATH=/opt ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1 - name: 'Wait for flipper to finish updating' id: connect if: steps.compile.outcome == 'success' - continue-on-error: true run: | python3 ./scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}} - name: 'Format flipper SD card' id: format if: steps.connect.outcome == 'success' - continue-on-error: true run: | ./scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext - - name: 'Copy unit tests to flipper' + - name: 'Copy assets and unit tests data to flipper' id: copy if: steps.format.outcome == 'success' - continue-on-error: true run: | + ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/resources /ext ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/unit_tests /ext/unit_tests - name: 'Run units and validate results' if: steps.copy.outcome == 'success' - continue-on-error: true run: | python3 ./scripts/testing/units.py ${{steps.device.outputs.flipper}} From 0349380347406d80d4a97829b3380c7a42dfc93a Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:46:40 +0300 Subject: [PATCH 07/14] update i2c tools --- .../plugins/flipper_i2ctools/i2cscanner.c | 35 + .../plugins/flipper_i2ctools/i2cscanner.h | 23 + .../plugins/flipper_i2ctools/i2csender.c | 29 + .../plugins/flipper_i2ctools/i2csender.h | 21 + .../plugins/flipper_i2ctools/i2csniffer.c | 99 +++ .../plugins/flipper_i2ctools/i2csniffer.h | 45 ++ .../plugins/flipper_i2ctools/i2ctools.c | 665 +++--------------- .../plugins/flipper_i2ctools/i2ctools_i.h | 22 + .../flipper_i2ctools/views/main_view.c | 63 ++ .../flipper_i2ctools/views/main_view.h | 38 + .../flipper_i2ctools/views/scanner_view.c | 55 ++ .../flipper_i2ctools/views/scanner_view.h | 11 + .../flipper_i2ctools/views/sender_view.c | 52 ++ .../flipper_i2ctools/views/sender_view.h | 11 + .../flipper_i2ctools/views/sniffer_view.c | 62 ++ .../flipper_i2ctools/views/sniffer_view.h | 11 + 16 files changed, 674 insertions(+), 568 deletions(-) create mode 100644 applications/plugins/flipper_i2ctools/i2cscanner.c create mode 100644 applications/plugins/flipper_i2ctools/i2cscanner.h create mode 100644 applications/plugins/flipper_i2ctools/i2csender.c create mode 100644 applications/plugins/flipper_i2ctools/i2csender.h create mode 100644 applications/plugins/flipper_i2ctools/i2csniffer.c create mode 100644 applications/plugins/flipper_i2ctools/i2csniffer.h create mode 100644 applications/plugins/flipper_i2ctools/i2ctools_i.h create mode 100644 applications/plugins/flipper_i2ctools/views/main_view.c create mode 100644 applications/plugins/flipper_i2ctools/views/main_view.h create mode 100644 applications/plugins/flipper_i2ctools/views/scanner_view.c create mode 100644 applications/plugins/flipper_i2ctools/views/scanner_view.h create mode 100644 applications/plugins/flipper_i2ctools/views/sender_view.c create mode 100644 applications/plugins/flipper_i2ctools/views/sender_view.h create mode 100644 applications/plugins/flipper_i2ctools/views/sniffer_view.c create mode 100644 applications/plugins/flipper_i2ctools/views/sniffer_view.h diff --git a/applications/plugins/flipper_i2ctools/i2cscanner.c b/applications/plugins/flipper_i2ctools/i2cscanner.c new file mode 100644 index 000000000..0ed949621 --- /dev/null +++ b/applications/plugins/flipper_i2ctools/i2cscanner.c @@ -0,0 +1,35 @@ +#include "i2cscanner.h" + +void scan_i2c_bus(i2cScanner* i2c_scanner) { + i2c_scanner->nb_found = 0; + i2c_scanner->scanned = true; + // Get the bus + furi_hal_i2c_acquire(I2C_BUS); + // scan + for(uint8_t addr = 0x01; addr < MAX_I2C_ADDR; addr++) { + // Check for peripherals + if(furi_hal_i2c_is_device_ready(I2C_BUS, addr, I2C_TIMEOUT)) { + // skip even 8-bit addr + if(addr % 2 != 0) { + continue; + } + // convert addr to 7-bits + i2c_scanner->addresses[i2c_scanner->nb_found] = addr >> 1; + i2c_scanner->nb_found++; + } + } + furi_hal_i2c_release(I2C_BUS); +} + +i2cScanner* i2c_scanner_alloc() { + i2cScanner* i2c_scanner = malloc(sizeof(i2cScanner)); + i2c_scanner->nb_found = 0; + i2c_scanner->menu_index = 0; + i2c_scanner->scanned = false; + return i2c_scanner; +} + +void i2c_scanner_free(i2cScanner* i2c_scanner) { + furi_assert(i2c_scanner); + free(i2c_scanner); +} \ No newline at end of file diff --git a/applications/plugins/flipper_i2ctools/i2cscanner.h b/applications/plugins/flipper_i2ctools/i2cscanner.h new file mode 100644 index 000000000..5320ebb9e --- /dev/null +++ b/applications/plugins/flipper_i2ctools/i2cscanner.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +// I2C BUS +#define I2C_BUS &furi_hal_i2c_handle_external +#define I2C_TIMEOUT 3 + +// 7 bits addresses +#define MAX_I2C_ADDR 0x7F + +typedef struct { + uint8_t addresses[MAX_I2C_ADDR + 1]; + uint8_t nb_found; + uint8_t menu_index; + bool scanned; +} i2cScanner; + +void scan_i2c_bus(i2cScanner* i2c_scanner); + +i2cScanner* i2c_scanner_alloc(); +void i2c_scanner_free(i2cScanner* i2c_scanner); diff --git a/applications/plugins/flipper_i2ctools/i2csender.c b/applications/plugins/flipper_i2ctools/i2csender.c new file mode 100644 index 000000000..bdc98cd9e --- /dev/null +++ b/applications/plugins/flipper_i2ctools/i2csender.c @@ -0,0 +1,29 @@ +#include "i2csender.h" + +void i2c_send(i2cSender* i2c_sender) { + furi_hal_i2c_acquire(I2C_BUS); + uint8_t adress = i2c_sender->scanner->addresses[i2c_sender->address_idx] << 1; + i2c_sender->error = furi_hal_i2c_trx( + I2C_BUS, + adress, + &i2c_sender->value, + sizeof(i2c_sender->value), + i2c_sender->recv, + sizeof(i2c_sender->recv), + I2C_TIMEOUT); + furi_hal_i2c_release(I2C_BUS); + i2c_sender->must_send = false; + i2c_sender->sended = true; +} + +i2cSender* i2c_sender_alloc() { + i2cSender* i2c_sender = malloc(sizeof(i2cSender)); + i2c_sender->must_send = false; + i2c_sender->sended = false; + return i2c_sender; +} + +void i2c_sender_free(i2cSender* i2c_sender) { + furi_assert(i2c_sender); + free(i2c_sender); +} \ No newline at end of file diff --git a/applications/plugins/flipper_i2ctools/i2csender.h b/applications/plugins/flipper_i2ctools/i2csender.h new file mode 100644 index 000000000..2aa74d6e2 --- /dev/null +++ b/applications/plugins/flipper_i2ctools/i2csender.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include "i2cscanner.h" + +typedef struct { + uint8_t address_idx; + uint8_t value; + uint8_t recv[2]; + bool must_send; + bool sended; + bool error; + + i2cScanner* scanner; +} i2cSender; + +void i2c_send(); + +i2cSender* i2c_sender_alloc(); +void i2c_sender_free(i2cSender* i2c_sender); diff --git a/applications/plugins/flipper_i2ctools/i2csniffer.c b/applications/plugins/flipper_i2ctools/i2csniffer.c new file mode 100644 index 000000000..b737a2be9 --- /dev/null +++ b/applications/plugins/flipper_i2ctools/i2csniffer.c @@ -0,0 +1,99 @@ +#include "i2csniffer.h" + +void clear_sniffer_buffers(i2cSniffer* i2c_sniffer) { + furi_assert(i2c_sniffer); + for(uint8_t i = 0; i < MAX_RECORDS; i++) { + for(uint8_t j = 0; j < MAX_MESSAGE_SIZE; j++) { + i2c_sniffer->frames[i].ack[j] = false; + i2c_sniffer->frames[i].data[j] = 0; + } + i2c_sniffer->frames[i].bit_index = 0; + i2c_sniffer->frames[i].data_index = 0; + } + i2c_sniffer->frame_index = 0; + i2c_sniffer->state = I2C_BUS_FREE; + i2c_sniffer->first = true; +} + +void start_interrupts(i2cSniffer* i2c_sniffer) { + furi_assert(i2c_sniffer); + furi_hal_gpio_init(pinSCL, GpioModeInterruptRise, GpioPullNo, GpioSpeedHigh); + furi_hal_gpio_add_int_callback(pinSCL, SCLcallback, i2c_sniffer); + + // Add Rise and Fall Interrupt on SDA pin + furi_hal_gpio_init(pinSDA, GpioModeInterruptRiseFall, GpioPullNo, GpioSpeedHigh); + furi_hal_gpio_add_int_callback(pinSDA, SDAcallback, i2c_sniffer); +} + +void stop_interrupts() { + furi_hal_gpio_remove_int_callback(pinSCL); + furi_hal_gpio_remove_int_callback(pinSDA); + // Reset GPIO pins to default state + furi_hal_gpio_init(pinSCL, GpioModeAnalog, GpioPullNo, GpioSpeedLow); + furi_hal_gpio_init(pinSDA, GpioModeAnalog, GpioPullNo, GpioSpeedLow); +} + +// Called on Fallin/Rising SDA +// Used to monitor i2c bus state +void SDAcallback(void* _i2c_sniffer) { + i2cSniffer* i2c_sniffer = _i2c_sniffer; + // SCL is low maybe cclock strecching + if(furi_hal_gpio_read(pinSCL) == false) { + return; + } + // Check for stop condition: SDA rising while SCL is High + if(i2c_sniffer->state == I2C_BUS_STARTED) { + if(furi_hal_gpio_read(pinSDA) == true) { + i2c_sniffer->state = I2C_BUS_FREE; + } + } + // Check for start condition: SDA falling while SCL is high + else if(furi_hal_gpio_read(pinSDA) == false) { + i2c_sniffer->state = I2C_BUS_STARTED; + if(i2c_sniffer->first) { + i2c_sniffer->first = false; + return; + } + i2c_sniffer->frame_index++; + if(i2c_sniffer->frame_index >= MAX_RECORDS) { + clear_sniffer_buffers(i2c_sniffer); + } + } + return; +} + +// Called on Rising SCL +// Used to read bus datas +void SCLcallback(void* _i2c_sniffer) { + i2cSniffer* i2c_sniffer = _i2c_sniffer; + if(i2c_sniffer->state == I2C_BUS_FREE) { + return; + } + uint8_t frame = i2c_sniffer->frame_index; + uint8_t bit = i2c_sniffer->frames[frame].bit_index; + uint8_t data_idx = i2c_sniffer->frames[frame].data_index; + if(bit < 8) { + i2c_sniffer->frames[frame].data[data_idx] <<= 1; + i2c_sniffer->frames[frame].data[data_idx] |= (int)furi_hal_gpio_read(pinSDA); + i2c_sniffer->frames[frame].bit_index++; + } else { + i2c_sniffer->frames[frame].ack[data_idx] = !furi_hal_gpio_read(pinSDA); + i2c_sniffer->frames[frame].data_index++; + i2c_sniffer->frames[frame].bit_index = 0; + } +} + +i2cSniffer* i2c_sniffer_alloc() { + i2cSniffer* i2c_sniffer = malloc(sizeof(i2cSniffer)); + i2c_sniffer->started = false; + clear_sniffer_buffers(i2c_sniffer); + return i2c_sniffer; +} + +void i2c_sniffer_free(i2cSniffer* i2c_sniffer) { + furi_assert(i2c_sniffer); + if(i2c_sniffer->started) { + stop_interrupts(); + } + free(i2c_sniffer); +} diff --git a/applications/plugins/flipper_i2ctools/i2csniffer.h b/applications/plugins/flipper_i2ctools/i2csniffer.h new file mode 100644 index 000000000..d7fd0df07 --- /dev/null +++ b/applications/plugins/flipper_i2ctools/i2csniffer.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +// I2C Pins +#define pinSCL &gpio_ext_pc0 +#define pinSDA &gpio_ext_pc1 + +// Bus States +typedef enum { I2C_BUS_FREE, I2C_BUS_STARTED } i2cBusStates; + +// Max read size of i2c frame by message +// Arbitraly defined +// They're not real limit to maximum frames send +#define MAX_MESSAGE_SIZE 128 + +// Nb of records +#define MAX_RECORDS 32 + +/// @brief Struct used to store our reads +typedef struct { + uint8_t data[MAX_MESSAGE_SIZE]; + bool ack[MAX_MESSAGE_SIZE]; + uint8_t bit_index; + uint8_t data_index; +} i2cFrame; + +typedef struct { + bool started; + bool first; + i2cBusStates state; + i2cFrame frames[MAX_RECORDS]; + uint8_t frame_index; + uint8_t menu_index; +} i2cSniffer; + +void clear_sniffer_buffers(i2cSniffer* i2c_sniffer); +void start_interrupts(i2cSniffer* i2c_sniffer); +void stop_interrupts(); +void SDAcallback(void* _i2c_sniffer); +void SCLcallback(void* _i2c_sniffer); + +i2cSniffer* i2c_sniffer_alloc(); +void i2c_sniffer_free(i2cSniffer* i2c_sniffer); \ No newline at end of file diff --git a/applications/plugins/flipper_i2ctools/i2ctools.c b/applications/plugins/flipper_i2ctools/i2ctools.c index d34f1dd7d..810750236 100644 --- a/applications/plugins/flipper_i2ctools/i2ctools.c +++ b/applications/plugins/flipper_i2ctools/i2ctools.c @@ -1,490 +1,29 @@ -#include -#include -#include -#include -#include -#include - -#define MAX_I2C_ADDR 0x7F - -#define APP_NAME "I2C Tools" - -#define SCAN_MENU_TEXT "Scan" -#define SCAN_MENU_X 75 -#define SCAN_MENU_Y 6 - -#define SNIFF_MENU_TEXT "Sniff" -#define SNIFF_MENU_X 75 -#define SNIFF_MENU_Y 20 - -#define SEND_MENU_TEXT "Send" -#define SEND_MENU_X 75 -#define SEND_MENU_Y 34 - -#define PLAY_MENU_TEXT "Play" -#define PLAY_MENU_X 75 -#define PLAY_MENU_Y 48 - -// Sniffer Pins -#define pinSCL &gpio_ext_pc0 -#define pinSDA &gpio_ext_pc1 - -// I2C BUS -#define I2C_BUS &furi_hal_i2c_handle_external - -typedef enum { - MAIN_VIEW, - SCAN_VIEW, - SNIFF_VIEW, - SEND_VIEW, - //PLAY_VIEW, - - /* Know menu Size*/ - MENU_SIZE -} i2cToolsMainMenu; - -// Bus Sniffer -typedef enum { I2C_BUS_IDLE, I2C_BUS_STARTED } i2cBusStates; - -#define MAX_FRAMES 32 - -typedef struct { - uint8_t data[MAX_FRAMES]; - bool ack[MAX_FRAMES]; - uint8_t bit_index; - uint8_t data_index; -} i2cFrame; - -typedef struct { - bool started; - bool first; - i2cBusStates state; - i2cFrame frames[MAX_FRAMES]; - uint8_t frame_index; - uint8_t menu_index; -} _sniffer; - -// Bus scanner -typedef struct { - uint8_t addresses[MAX_I2C_ADDR + 1]; - uint8_t found; - uint8_t menu_index; - bool scanned; -} _scanner; - -// Sender -typedef struct { - uint8_t address_idx; - uint8_t value; - uint8_t recv[2]; - bool must_send; - bool sended; - bool error; -} _sender; - -typedef struct { - ViewPort* view_port; - i2cToolsMainMenu current_menu; - NotificationApp* notifications; // Used to blink LED - uint8_t main_menu_index; - - _scanner scanner; - _sniffer sniffer; - _sender sender; -} i2cToolsData; - -void scan_i2c_bus(i2cToolsData* data) { - data->scanner.found = 0; - data->scanner.scanned = true; - furi_hal_i2c_acquire(I2C_BUS); - // scan - for(uint8_t addr = 0x01; addr < MAX_I2C_ADDR; addr++) { - // Check for peripherals - if(furi_hal_i2c_is_device_ready(I2C_BUS, addr, 2)) { - // skip even 8-bit addr - if(addr % 2 != 0) { - continue; - } - // convert addr to 7-bits - data->scanner.addresses[data->scanner.found] = addr >> 1; - data->scanner.found++; - } - } - furi_hal_i2c_release(I2C_BUS); -} - -void i2ctools_draw_main_menu(Canvas* canvas, i2cToolsData* data) { - canvas_clear(canvas); - canvas_set_color(canvas, ColorBlack); - canvas_draw_rframe(canvas, 0, 0, 128, 64, 3); - canvas_draw_icon(canvas, 2, 13, &I_passport_bad3_46x49); - canvas_set_font(canvas, FontPrimary); - canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, APP_NAME); - - switch(data->main_menu_index) { - case 0: - canvas_set_color(canvas, ColorBlack); - canvas_draw_str_aligned( - canvas, SNIFF_MENU_X, SNIFF_MENU_Y, AlignLeft, AlignTop, SNIFF_MENU_TEXT); - canvas_draw_str_aligned( - canvas, SEND_MENU_X, SEND_MENU_Y, AlignLeft, AlignTop, SEND_MENU_TEXT); - /*canvas_draw_str_aligned( - canvas, PLAY_MENU_X, PLAY_MENU_Y, AlignLeft, AlignTop, PLAY_MENU_TEXT);*/ - - canvas_draw_rbox(canvas, 60, SCAN_MENU_Y - 2, 60, 13, 3); - canvas_set_color(canvas, ColorWhite); - canvas_draw_str_aligned( - canvas, SCAN_MENU_X, SCAN_MENU_Y, AlignLeft, AlignTop, SCAN_MENU_TEXT); - break; - - case 1: - canvas_set_color(canvas, ColorBlack); - canvas_draw_str_aligned( - canvas, SCAN_MENU_X, SCAN_MENU_Y, AlignLeft, AlignTop, SCAN_MENU_TEXT); - canvas_draw_str_aligned( - canvas, SEND_MENU_X, SEND_MENU_Y, AlignLeft, AlignTop, SEND_MENU_TEXT); - /*canvas_draw_str_aligned( - canvas, PLAY_MENU_X, PLAY_MENU_Y, AlignLeft, AlignTop, PLAY_MENU_TEXT);*/ - - canvas_draw_rbox(canvas, 60, SNIFF_MENU_Y - 2, 60, 13, 3); - canvas_set_color(canvas, ColorWhite); - canvas_draw_str_aligned( - canvas, SNIFF_MENU_X, SNIFF_MENU_Y, AlignLeft, AlignTop, SNIFF_MENU_TEXT); - break; - - case 2: - canvas_set_color(canvas, ColorBlack); - canvas_draw_str_aligned( - canvas, SCAN_MENU_X, SCAN_MENU_Y, AlignLeft, AlignTop, SCAN_MENU_TEXT); - canvas_draw_str_aligned( - canvas, SNIFF_MENU_X, SNIFF_MENU_Y, AlignLeft, AlignTop, SNIFF_MENU_TEXT); - /*canvas_draw_str_aligned( - canvas, PLAY_MENU_X, PLAY_MENU_Y, AlignLeft, AlignTop, PLAY_MENU_TEXT);*/ - - canvas_draw_rbox(canvas, 60, SEND_MENU_Y - 2, 60, 13, 3); - canvas_set_color(canvas, ColorWhite); - canvas_draw_str_aligned( - canvas, SEND_MENU_X, SEND_MENU_Y, AlignLeft, AlignTop, SEND_MENU_TEXT); - break; - - case 3: - canvas_set_color(canvas, ColorBlack); - canvas_draw_str_aligned( - canvas, SCAN_MENU_X, SCAN_MENU_Y, AlignLeft, AlignTop, SCAN_MENU_TEXT); - canvas_draw_str_aligned( - canvas, SNIFF_MENU_X, SNIFF_MENU_Y, AlignLeft, AlignTop, SNIFF_MENU_TEXT); - canvas_draw_str_aligned( - canvas, SEND_MENU_X, SEND_MENU_Y, AlignLeft, AlignTop, SEND_MENU_TEXT); - - canvas_draw_rbox(canvas, 60, PLAY_MENU_Y - 2, 60, 13, 3); - canvas_set_color(canvas, ColorWhite); - canvas_draw_str_aligned( - canvas, PLAY_MENU_X, PLAY_MENU_Y, AlignLeft, AlignTop, PLAY_MENU_TEXT); - break; - - default: - break; - } -} - -void clearSnifferBuffers(void* ctx) { - i2cToolsData* data = ctx; - for(uint8_t i = 0; i < MAX_FRAMES; i++) { - for(uint8_t j = 0; j < MAX_FRAMES; j++) { - data->sniffer.frames[i].ack[j] = false; - data->sniffer.frames[i].data[j] = 0; - } - data->sniffer.frames[i].bit_index = 0; - data->sniffer.frames[i].data_index = 0; - } - data->sniffer.frame_index = 0; - data->sniffer.state = I2C_BUS_IDLE; - data->sniffer.first = true; -} - -// Called on Fallin/Rising SDA -// Used to monitor i2c bus state -static void SDAcallback(void* ctx) { - i2cToolsData* data = ctx; - // SCL is low maybe cclock strecching - if(furi_hal_gpio_read(pinSCL) == false) { - return; - } - // Check for stop condition: SDA rising while SCL is High - if(data->sniffer.state == I2C_BUS_STARTED) { - if(furi_hal_gpio_read(pinSDA) == true) { - data->sniffer.state = I2C_BUS_IDLE; - view_port_update(data->view_port); - } - } - // Check for start condition: SDA falling while SCL is high - else if(furi_hal_gpio_read(pinSDA) == false) { - data->sniffer.state = I2C_BUS_STARTED; - if(data->sniffer.first) { - data->sniffer.first = false; - return; - } - data->sniffer.frame_index++; - if(data->sniffer.frame_index >= MAX_FRAMES) { - clearSnifferBuffers(ctx); - } - } - return; -} - -// Called on Rising SCL -// Used to read bus datas -static void SCLcallback(void* ctx) { - i2cToolsData* data = ctx; - if(data->sniffer.state == I2C_BUS_IDLE) { - return; - } - uint8_t frame = data->sniffer.frame_index; - uint8_t bit = data->sniffer.frames[frame].bit_index; - uint8_t data_idx = data->sniffer.frames[frame].data_index; - if(bit < 8) { - data->sniffer.frames[frame].data[data_idx] <<= 1; - data->sniffer.frames[frame].data[data_idx] |= (int)furi_hal_gpio_read(pinSDA); - data->sniffer.frames[frame].bit_index++; - } else { - data->sniffer.frames[frame].ack[data_idx] = !furi_hal_gpio_read(pinSDA); - data->sniffer.frames[frame].data_index++; - data->sniffer.frames[frame].bit_index = 0; - } -} - -void start_interrupts(i2cToolsData* data) { - furi_hal_gpio_init(pinSCL, GpioModeInterruptRise, GpioPullNo, GpioSpeedHigh); - furi_hal_gpio_add_int_callback(pinSCL, SCLcallback, data); - - // Add Rise and Fall Interrupt on SDA pin - furi_hal_gpio_init(pinSDA, GpioModeInterruptRiseFall, GpioPullNo, GpioSpeedHigh); - furi_hal_gpio_add_int_callback(pinSDA, SDAcallback, data); -} - -void stop_interrupts() { - furi_hal_gpio_remove_int_callback(pinSCL); - furi_hal_gpio_remove_int_callback(pinSDA); -} - -void i2ctools_draw_sniff_view(Canvas* canvas, i2cToolsData* data) { - canvas_clear(canvas); - canvas_set_color(canvas, ColorBlack); - canvas_draw_rframe(canvas, 0, 0, 128, 64, 3); - canvas_draw_icon(canvas, 2, 13, &I_passport_happy2_46x49); - - canvas_set_font(canvas, FontPrimary); - canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, SNIFF_MENU_TEXT); - canvas_set_font(canvas, FontSecondary); - - // Button - canvas_draw_rbox(canvas, 70, 48, 45, 13, 3); - canvas_set_color(canvas, ColorWhite); - canvas_draw_icon(canvas, 75, 50, &I_Ok_btn_9x9); - if(!data->sniffer.started) { - canvas_draw_str_aligned(canvas, 85, 51, AlignLeft, AlignTop, "Start"); - } else { - canvas_draw_str_aligned(canvas, 85, 51, AlignLeft, AlignTop, "Stop"); - } - canvas_set_color(canvas, ColorBlack); - // Address text - char addr_text[8]; - snprintf( - addr_text, - sizeof(addr_text), - "0x%02x", - (int)(data->sniffer.frames[data->sniffer.menu_index].data[0] >> 1)); - canvas_draw_str_aligned(canvas, 50, 3, AlignLeft, AlignTop, "Addr: "); - canvas_draw_str_aligned(canvas, 75, 3, AlignLeft, AlignTop, addr_text); - // R/W - if((int)(data->sniffer.frames[data->sniffer.menu_index].data[0]) % 2 == 0) { - canvas_draw_str_aligned(canvas, 105, 3, AlignLeft, AlignTop, "W"); - } else { - canvas_draw_str_aligned(canvas, 105, 3, AlignLeft, AlignTop, "R"); - } - // nbFrame text - canvas_draw_str_aligned(canvas, 50, 13, AlignLeft, AlignTop, "Frames: "); - snprintf(addr_text, sizeof(addr_text), "%d", (int)data->sniffer.menu_index + 1); - canvas_draw_str_aligned(canvas, 90, 13, AlignLeft, AlignTop, addr_text); - canvas_draw_str_aligned(canvas, 100, 13, AlignLeft, AlignTop, "/"); - snprintf(addr_text, sizeof(addr_text), "%d", (int)data->sniffer.frame_index + 1); - canvas_draw_str_aligned(canvas, 110, 13, AlignLeft, AlignTop, addr_text); - // Frames content - uint8_t x_pos = 0; - uint8_t y_pos = 23; - for(uint8_t i = 1; i < data->sniffer.frames[data->sniffer.menu_index].data_index; i++) { - snprintf( - addr_text, - sizeof(addr_text), - "0x%02x", - (int)data->sniffer.frames[data->sniffer.menu_index].data[i]); - x_pos = 50 + (i - 1) * 35; - canvas_draw_str_aligned(canvas, x_pos, y_pos, AlignLeft, AlignTop, addr_text); - if(data->sniffer.frames[data->sniffer.menu_index].ack[i]) { - canvas_draw_str_aligned(canvas, x_pos + 24, y_pos, AlignLeft, AlignTop, "A"); - } else { - canvas_draw_str_aligned(canvas, x_pos + 24, y_pos, AlignLeft, AlignTop, "N"); - } - } -} - -void i2ctools_draw_record_view(Canvas* canvas, i2cToolsData* data) { - UNUSED(data); - canvas_clear(canvas); - canvas_set_color(canvas, ColorBlack); - canvas_draw_rframe(canvas, 0, 0, 128, 64, 3); - canvas_draw_icon(canvas, 2, 13, &I_passport_happy2_46x49); - - canvas_set_font(canvas, FontPrimary); - canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, PLAY_MENU_TEXT); -} - -void i2ctools_draw_send_view(Canvas* canvas, i2cToolsData* data) { - canvas_clear(canvas); - canvas_set_color(canvas, ColorBlack); - canvas_draw_rframe(canvas, 0, 0, 128, 64, 3); - canvas_draw_icon(canvas, 2, 13, &I_passport_happy2_46x49); - canvas_set_font(canvas, FontPrimary); - canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, SEND_MENU_TEXT); - - if(!data->scanner.scanned) { - scan_i2c_bus(data); - } - - canvas_set_font(canvas, FontSecondary); - if(data->scanner.found <= 0) { - canvas_draw_str_aligned(canvas, 60, 5, AlignLeft, AlignTop, "No peripherals"); - canvas_draw_str_aligned(canvas, 60, 15, AlignLeft, AlignTop, "Found"); - return; - } - canvas_draw_rbox(canvas, 70, 48, 45, 13, 3); - canvas_set_color(canvas, ColorWhite); - canvas_draw_icon(canvas, 75, 50, &I_Ok_btn_9x9); - canvas_draw_str_aligned(canvas, 85, 51, AlignLeft, AlignTop, "Send"); - canvas_set_color(canvas, ColorBlack); - canvas_draw_str_aligned(canvas, 50, 5, AlignLeft, AlignTop, "Addr: "); - canvas_draw_icon(canvas, 80, 5, &I_ButtonLeft_4x7); - canvas_draw_icon(canvas, 115, 5, &I_ButtonRight_4x7); - char addr_text[8]; - snprintf( - addr_text, - sizeof(addr_text), - "0x%02x", - (int)data->scanner.addresses[data->sender.address_idx]); - canvas_draw_str_aligned(canvas, 90, 5, AlignLeft, AlignTop, addr_text); - canvas_draw_str_aligned(canvas, 50, 15, AlignLeft, AlignTop, "Value: "); - - canvas_draw_icon(canvas, 80, 17, &I_ButtonUp_7x4); - canvas_draw_icon(canvas, 115, 17, &I_ButtonDown_7x4); - snprintf(addr_text, sizeof(addr_text), "0x%02x", (int)data->sender.value); - canvas_draw_str_aligned(canvas, 90, 15, AlignLeft, AlignTop, addr_text); - if(data->sender.must_send) { - furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); - data->sender.error = furi_hal_i2c_trx( - &furi_hal_i2c_handle_external, - data->scanner.addresses[data->sender.address_idx] << 1, - &data->sender.value, - 1, - data->sender.recv, - sizeof(data->sender.recv), - 3); - furi_hal_i2c_release(&furi_hal_i2c_handle_external); - data->sender.must_send = false; - data->sender.sended = true; - } - canvas_draw_str_aligned(canvas, 50, 25, AlignLeft, AlignTop, "Result: "); - if(data->sender.sended) { - //if(data->sender.error) { - for(uint8_t i = 0; i < sizeof(data->sender.recv); i++) { - snprintf(addr_text, sizeof(addr_text), "0x%02x", (int)data->sender.recv[i]); - canvas_draw_str_aligned(canvas, 90, 25 + (i * 10), AlignLeft, AlignTop, addr_text); - } - /* - } else { - canvas_draw_str_aligned(canvas, 90, 25, AlignLeft, AlignTop, "Error"); - }*/ - } -} - -void i2ctools_draw_scan_view(Canvas* canvas, i2cToolsData* data) { - canvas_clear(canvas); - canvas_set_color(canvas, ColorBlack); - canvas_draw_rframe(canvas, 0, 0, 128, 64, 3); - canvas_draw_icon(canvas, 2, 13, &I_passport_happy3_46x49); - canvas_set_font(canvas, FontPrimary); - canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, SCAN_MENU_TEXT); - - char count_text[46]; - char count_text_fmt[] = "Found: %d"; - canvas_set_font(canvas, FontSecondary); - snprintf(count_text, sizeof(count_text), count_text_fmt, (int)data->scanner.found); - canvas_draw_str_aligned(canvas, 50, 3, AlignLeft, AlignTop, count_text); - uint8_t x_pos = 0; - uint8_t y_pos = 0; - uint8_t idx_to_print = 0; - for(uint8_t i = 0; i < (int)data->scanner.found; i++) { - idx_to_print = i + data->scanner.menu_index * 3; - if(idx_to_print >= MAX_I2C_ADDR) { - break; - } - snprintf( - count_text, sizeof(count_text), "0x%02x ", (int)data->scanner.addresses[idx_to_print]); - if(i < 3) { - x_pos = 50 + (i * 26); - y_pos = 15; - } else if(i < 6) { - x_pos = 50 + ((i - 3) * 26); - y_pos = 25; - } else if(i < 9) { - x_pos = 50 + ((i - 6) * 26); - y_pos = 35; - } else if(i < 12) { - x_pos = 50 + ((i - 9) * 26); - y_pos = 45; - } else if(i < 15) { - x_pos = 50 + ((i - 12) * 26); - y_pos = 55; - } else { - break; - } - canvas_draw_str_aligned(canvas, x_pos, y_pos, AlignLeft, AlignTop, count_text); - } - // Right cursor - y_pos = 14 + data->scanner.menu_index; - canvas_draw_rbox(canvas, 125, y_pos, 3, 10, 1); - - // Button - canvas_draw_rbox(canvas, 70, 48, 45, 13, 3); - canvas_set_color(canvas, ColorWhite); - canvas_draw_icon(canvas, 75, 50, &I_Ok_btn_9x9); - canvas_draw_str_aligned(canvas, 85, 51, AlignLeft, AlignTop, "Scan"); -} +#include "i2ctools_i.h" void i2ctools_draw_callback(Canvas* canvas, void* ctx) { - i2cToolsData* i2c_addr = acquire_mutex((ValueMutex*)ctx, 25); + i2cTools* i2ctools = acquire_mutex((ValueMutex*)ctx, 25); - switch(i2c_addr->current_menu) { + switch(i2ctools->main_view->current_view) { case MAIN_VIEW: - i2ctools_draw_main_menu(canvas, i2c_addr); + draw_main_view(canvas, i2ctools->main_view); break; case SCAN_VIEW: - i2ctools_draw_scan_view(canvas, i2c_addr); + draw_scanner_view(canvas, i2ctools->scanner); break; case SNIFF_VIEW: - i2ctools_draw_sniff_view(canvas, i2c_addr); + draw_sniffer_view(canvas, i2ctools->sniffer); break; + case SEND_VIEW: - i2ctools_draw_send_view(canvas, i2c_addr); + draw_sender_view(canvas, i2ctools->sender); break; - /*case PLAY_VIEW: - i2ctools_draw_record_view(canvas, i2c_addr); - break;*/ + default: break; } - release_mutex((ValueMutex*)ctx, i2c_addr); + release_mutex((ValueMutex*)ctx, i2ctools); } void i2ctools_input_callback(InputEvent* input_event, void* ctx) { @@ -497,157 +36,147 @@ int32_t i2ctools_app(void* p) { UNUSED(p); FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); - i2cToolsData* i2caddrs = malloc(sizeof(i2cToolsData)); - ValueMutex i2caddrs_mutex; - if(!init_mutex(&i2caddrs_mutex, i2caddrs, sizeof(i2cToolsData))) { + // Alloc i2ctools + i2cTools* i2ctools = malloc(sizeof(i2cTools)); + ValueMutex i2ctools_mutex; + if(!init_mutex(&i2ctools_mutex, i2ctools, sizeof(i2cTools))) { FURI_LOG_E(APP_NAME, "cannot create mutex\r\n"); - free(i2caddrs); + free(i2ctools); return -1; } - printf(APP_NAME); - printf("\r\n"); - // i2caddrs->notifications = furi_record_open(RECORD_NOTIFICATION); - i2caddrs->view_port = view_port_alloc(); - view_port_draw_callback_set(i2caddrs->view_port, i2ctools_draw_callback, &i2caddrs_mutex); - view_port_input_callback_set(i2caddrs->view_port, i2ctools_input_callback, event_queue); + // Alloc viewport + i2ctools->view_port = view_port_alloc(); + view_port_draw_callback_set(i2ctools->view_port, i2ctools_draw_callback, &i2ctools_mutex); + view_port_input_callback_set(i2ctools->view_port, i2ctools_input_callback, event_queue); // Register view port in GUI Gui* gui = furi_record_open(RECORD_GUI); - gui_add_view_port(gui, i2caddrs->view_port, GuiLayerFullscreen); + gui_add_view_port(gui, i2ctools->view_port, GuiLayerFullscreen); InputEvent event; - clearSnifferBuffers(i2caddrs); - i2caddrs->sniffer.started = false; - i2caddrs->sniffer.menu_index = 0; + i2ctools->main_view = i2c_main_view_alloc(); - i2caddrs->scanner.menu_index = 0; - i2caddrs->scanner.scanned = false; + i2ctools->sniffer = i2c_sniffer_alloc(); + i2ctools->sniffer->menu_index = 0; + + i2ctools->scanner = i2c_scanner_alloc(); + + i2ctools->sender = i2c_sender_alloc(); + // Share scanner with sender + i2ctools->sender->scanner = i2ctools->scanner; - i2caddrs->sender.must_send = false; - i2caddrs->sender.sended = false; while(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk) { if(event.key == InputKeyBack && event.type == InputTypeRelease) { - if(i2caddrs->current_menu == MAIN_VIEW) { + if(i2ctools->main_view->current_view == MAIN_VIEW) { break; } else { - if(i2caddrs->current_menu == SNIFF_VIEW) { + if(i2ctools->main_view->current_view == SNIFF_VIEW) { stop_interrupts(); - i2caddrs->sniffer.started = false; - i2caddrs->sniffer.state = I2C_BUS_IDLE; + i2ctools->sniffer->started = false; + i2ctools->sniffer->state = I2C_BUS_FREE; } - i2caddrs->current_menu = MAIN_VIEW; + i2ctools->main_view->current_view = MAIN_VIEW; } } else if(event.key == InputKeyUp && event.type == InputTypeRelease) { - if(i2caddrs->current_menu == MAIN_VIEW) { - if(i2caddrs->main_menu_index > 0) { - i2caddrs->main_menu_index--; + if(i2ctools->main_view->current_view == MAIN_VIEW) { + if((i2ctools->main_view->menu_index > SCAN_VIEW)) { + i2ctools->main_view->menu_index--; } - } else if(i2caddrs->current_menu == SCAN_VIEW) { - if(i2caddrs->scanner.menu_index > 0) { - i2caddrs->scanner.menu_index--; + } else if(i2ctools->main_view->current_view == SCAN_VIEW) { + if(i2ctools->scanner->menu_index > 0) { + i2ctools->scanner->menu_index--; } - } else if(i2caddrs->current_menu == SEND_VIEW) { - if(i2caddrs->sender.value < 0xFF) { - i2caddrs->sender.value++; - i2caddrs->sender.sended = false; + } else if(i2ctools->main_view->current_view == SEND_VIEW) { + if(i2ctools->sender->value < 0xFF) { + i2ctools->sender->value++; + i2ctools->sender->sended = false; } } } else if( event.key == InputKeyUp && (event.type == InputTypeLong || event.type == InputTypeRepeat)) { - if(i2caddrs->current_menu == SEND_VIEW) { - if(i2caddrs->sender.value < 0xF9) { - i2caddrs->sender.value += 5; - i2caddrs->sender.sended = false; + if(i2ctools->main_view->current_view == SEND_VIEW) { + if(i2ctools->sender->value < 0xF9) { + i2ctools->sender->value += 5; + i2ctools->sender->sended = false; } } - } else if(event.key == InputKeyDown && event.type == InputTypeRelease) { - if(i2caddrs->current_menu == MAIN_VIEW) { - if(i2caddrs->main_menu_index < 2) { - i2caddrs->main_menu_index++; + if(i2ctools->main_view->current_view == MAIN_VIEW) { + if(i2ctools->main_view->menu_index < MENU_SIZE) { + i2ctools->main_view->menu_index++; } - } else if(i2caddrs->current_menu == SCAN_VIEW) { - if(i2caddrs->scanner.menu_index < ((int)i2caddrs->scanner.found / 3)) { - i2caddrs->scanner.menu_index++; + } else if(i2ctools->main_view->current_view == SCAN_VIEW) { + if(i2ctools->scanner->menu_index < ((int)i2ctools->scanner->nb_found / 3)) { + i2ctools->scanner->menu_index++; } - } else if(i2caddrs->current_menu == SEND_VIEW) { - if(i2caddrs->sender.value > 0x00) { - i2caddrs->sender.value--; - i2caddrs->sender.sended = false; + } else if(i2ctools->main_view->current_view == SEND_VIEW) { + if(i2ctools->sender->value > 0x00) { + i2ctools->sender->value--; + i2ctools->sender->sended = false; } } } else if(event.key == InputKeyDown && event.type == InputTypeLong) { - if(i2caddrs->current_menu == SEND_VIEW) { - if(i2caddrs->sender.value > 0x05) { - i2caddrs->sender.value -= 5; - i2caddrs->sender.sended = false; + if(i2ctools->main_view->current_view == SEND_VIEW) { + if(i2ctools->sender->value > 0x05) { + i2ctools->sender->value -= 5; + i2ctools->sender->sended = false; } } } else if(event.key == InputKeyOk && event.type == InputTypeRelease) { - if(i2caddrs->current_menu == MAIN_VIEW) { - if(i2caddrs->main_menu_index == 0) { - scan_i2c_bus(i2caddrs); - i2caddrs->current_menu = SCAN_VIEW; - } else if(i2caddrs->main_menu_index == 1) { - i2caddrs->current_menu = SNIFF_VIEW; - } else if(i2caddrs->main_menu_index == 2) { - i2caddrs->current_menu = SEND_VIEW; - } /*else if(i2caddrs->main_menu_index == 3) { - i2caddrs->current_menu = PLAY_VIEW; - }*/ - } else if(i2caddrs->current_menu == SCAN_VIEW) { - scan_i2c_bus(i2caddrs); - } else if(i2caddrs->current_menu == SEND_VIEW) { - i2caddrs->sender.must_send = true; - } else if(i2caddrs->current_menu == SNIFF_VIEW) { - if(i2caddrs->sniffer.started) { + if(i2ctools->main_view->current_view == MAIN_VIEW) { + i2ctools->main_view->current_view = i2ctools->main_view->menu_index; + } else if(i2ctools->main_view->current_view == SCAN_VIEW) { + scan_i2c_bus(i2ctools->scanner); + } else if(i2ctools->main_view->current_view == SEND_VIEW) { + i2ctools->sender->must_send = true; + } else if(i2ctools->main_view->current_view == SNIFF_VIEW) { + if(i2ctools->sniffer->started) { stop_interrupts(); - i2caddrs->sniffer.started = false; - i2caddrs->sniffer.state = I2C_BUS_IDLE; + i2ctools->sniffer->started = false; + i2ctools->sniffer->state = I2C_BUS_FREE; } else { - start_interrupts(i2caddrs); - i2caddrs->sniffer.started = true; - i2caddrs->sniffer.state = I2C_BUS_IDLE; + start_interrupts(i2ctools->sniffer); + i2ctools->sniffer->started = true; + i2ctools->sniffer->state = I2C_BUS_FREE; } } } else if(event.key == InputKeyRight && event.type == InputTypeRelease) { - if(i2caddrs->current_menu == SEND_VIEW) { - if(i2caddrs->sender.address_idx < (i2caddrs->scanner.found - 1)) { - i2caddrs->sender.address_idx++; - i2caddrs->sender.sended = false; + if(i2ctools->main_view->current_view == SEND_VIEW) { + if(i2ctools->sender->address_idx < (i2ctools->scanner->nb_found - 1)) { + i2ctools->sender->address_idx++; + i2ctools->sender->sended = false; } - } else if(i2caddrs->current_menu == SNIFF_VIEW) { - if(i2caddrs->sniffer.menu_index < i2caddrs->sniffer.frame_index) { - i2caddrs->sniffer.menu_index++; + } else if(i2ctools->main_view->current_view == SNIFF_VIEW) { + if(i2ctools->sniffer->menu_index < i2ctools->sniffer->frame_index) { + i2ctools->sniffer->menu_index++; } } } else if(event.key == InputKeyLeft && event.type == InputTypeRelease) { - if(i2caddrs->current_menu == SEND_VIEW) { - if(i2caddrs->sender.address_idx > 0) { - i2caddrs->sender.address_idx--; - i2caddrs->sender.sended = false; + if(i2ctools->main_view->current_view == SEND_VIEW) { + if(i2ctools->sender->address_idx > 0) { + i2ctools->sender->address_idx--; + i2ctools->sender->sended = false; } - } else if(i2caddrs->current_menu == SNIFF_VIEW) { - if(i2caddrs->sniffer.menu_index > 0) { - i2caddrs->sniffer.menu_index--; + } else if(i2ctools->main_view->current_view == SNIFF_VIEW) { + if(i2ctools->sniffer->menu_index > 0) { + i2ctools->sniffer->menu_index--; } } } - view_port_update(i2caddrs->view_port); + view_port_update(i2ctools->view_port); } - // Reset GPIO pins to default state - furi_hal_gpio_init(&gpio_ext_pc0, GpioModeAnalog, GpioPullNo, GpioSpeedLow); - furi_hal_gpio_init(&gpio_ext_pc1, GpioModeAnalog, GpioPullNo, GpioSpeedLow); - gui_remove_view_port(gui, i2caddrs->view_port); - view_port_free(i2caddrs->view_port); + gui_remove_view_port(gui, i2ctools->view_port); + view_port_free(i2ctools->view_port); furi_message_queue_free(event_queue); - free(i2caddrs); - //furi_record_close(RECORD_NOTIFICATION); + i2c_sniffer_free(i2ctools->sniffer); + i2c_scanner_free(i2ctools->scanner); + i2c_sender_free(i2ctools->sender); + i2c_main_view_free(i2ctools->main_view); + free(i2ctools); furi_record_close(RECORD_GUI); - return 0; } diff --git a/applications/plugins/flipper_i2ctools/i2ctools_i.h b/applications/plugins/flipper_i2ctools/i2ctools_i.h new file mode 100644 index 000000000..33917dc34 --- /dev/null +++ b/applications/plugins/flipper_i2ctools/i2ctools_i.h @@ -0,0 +1,22 @@ +#include +#include +#include +#include + +#include "i2csniffer.h" +#include "i2cscanner.h" +#include "i2csender.h" +#include "views/main_view.h" +#include "views/sniffer_view.h" +#include "views/scanner_view.h" +#include "views/sender_view.h" + +// App datas +typedef struct { + ViewPort* view_port; + i2cMainView* main_view; + + i2cScanner* scanner; + i2cSniffer* sniffer; + i2cSender* sender; +} i2cTools; diff --git a/applications/plugins/flipper_i2ctools/views/main_view.c b/applications/plugins/flipper_i2ctools/views/main_view.c new file mode 100644 index 000000000..dfbc24e9b --- /dev/null +++ b/applications/plugins/flipper_i2ctools/views/main_view.c @@ -0,0 +1,63 @@ +#include "main_view.h" + +void draw_main_view(Canvas* canvas, i2cMainView* main_view) { + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_draw_rframe(canvas, 0, 0, 128, 64, 3); + canvas_draw_icon(canvas, 2, 13, &I_passport_bad3_46x49); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, APP_NAME); + + switch(main_view->menu_index) { + case SCAN_VIEW: + canvas_set_color(canvas, ColorBlack); + canvas_draw_str_aligned( + canvas, SNIFF_MENU_X, SNIFF_MENU_Y, AlignLeft, AlignTop, SNIFF_MENU_TEXT); + canvas_draw_str_aligned( + canvas, SEND_MENU_X, SEND_MENU_Y, AlignLeft, AlignTop, SEND_MENU_TEXT); + canvas_draw_rbox(canvas, 60, SCAN_MENU_Y - 2, 60, 13, 3); + canvas_set_color(canvas, ColorWhite); + canvas_draw_str_aligned( + canvas, SCAN_MENU_X, SCAN_MENU_Y, AlignLeft, AlignTop, SCAN_MENU_TEXT); + break; + + case SNIFF_VIEW: + canvas_set_color(canvas, ColorBlack); + canvas_draw_str_aligned( + canvas, SCAN_MENU_X, SCAN_MENU_Y, AlignLeft, AlignTop, SCAN_MENU_TEXT); + canvas_draw_str_aligned( + canvas, SEND_MENU_X, SEND_MENU_Y, AlignLeft, AlignTop, SEND_MENU_TEXT); + canvas_draw_rbox(canvas, 60, SNIFF_MENU_Y - 2, 60, 13, 3); + canvas_set_color(canvas, ColorWhite); + canvas_draw_str_aligned( + canvas, SNIFF_MENU_X, SNIFF_MENU_Y, AlignLeft, AlignTop, SNIFF_MENU_TEXT); + break; + + case SEND_VIEW: + canvas_set_color(canvas, ColorBlack); + canvas_draw_str_aligned( + canvas, SCAN_MENU_X, SCAN_MENU_Y, AlignLeft, AlignTop, SCAN_MENU_TEXT); + canvas_draw_str_aligned( + canvas, SNIFF_MENU_X, SNIFF_MENU_Y, AlignLeft, AlignTop, SNIFF_MENU_TEXT); + canvas_draw_rbox(canvas, 60, SEND_MENU_Y - 2, 60, 13, 3); + canvas_set_color(canvas, ColorWhite); + canvas_draw_str_aligned( + canvas, SEND_MENU_X, SEND_MENU_Y, AlignLeft, AlignTop, SEND_MENU_TEXT); + break; + + default: + break; + } +} + +i2cMainView* i2c_main_view_alloc() { + i2cMainView* main_view = malloc(sizeof(i2cMainView)); + main_view->menu_index = SCAN_VIEW; + main_view->current_view = MAIN_VIEW; + return main_view; +} + +void i2c_main_view_free(i2cMainView* main_view) { + furi_assert(main_view); + free(main_view); +} \ No newline at end of file diff --git a/applications/plugins/flipper_i2ctools/views/main_view.h b/applications/plugins/flipper_i2ctools/views/main_view.h new file mode 100644 index 000000000..7a604fc60 --- /dev/null +++ b/applications/plugins/flipper_i2ctools/views/main_view.h @@ -0,0 +1,38 @@ +#include +#include +#include + +#define APP_NAME "I2C Tools" + +#define SCAN_MENU_TEXT "Scan" +#define SCAN_MENU_X 75 +#define SCAN_MENU_Y 6 + +#define SNIFF_MENU_TEXT "Sniff" +#define SNIFF_MENU_X 75 +#define SNIFF_MENU_Y 20 + +#define SEND_MENU_TEXT "Send" +#define SEND_MENU_X 75 +#define SEND_MENU_Y 34 + +// Menu +typedef enum { + MAIN_VIEW, + SCAN_VIEW, + SNIFF_VIEW, + SEND_VIEW, + + /* Know menu Size*/ + MENU_SIZE +} i2cToolsViews; + +typedef struct { + i2cToolsViews current_view; + i2cToolsViews menu_index; +} i2cMainView; + +void draw_main_view(Canvas* canvas, i2cMainView* main_view); + +i2cMainView* i2c_main_view_alloc(); +void i2c_main_view_free(i2cMainView* main_view); \ No newline at end of file diff --git a/applications/plugins/flipper_i2ctools/views/scanner_view.c b/applications/plugins/flipper_i2ctools/views/scanner_view.c new file mode 100644 index 000000000..346f82590 --- /dev/null +++ b/applications/plugins/flipper_i2ctools/views/scanner_view.c @@ -0,0 +1,55 @@ +#include "scanner_view.h" + +void draw_scanner_view(Canvas* canvas, i2cScanner* i2c_scanner) { + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_draw_rframe(canvas, 0, 0, 128, 64, 3); + canvas_draw_icon(canvas, 2, 13, &I_passport_happy3_46x49); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, SCAN_MENU_TEXT); + + char count_text[46]; + char count_text_fmt[] = "Found: %d"; + canvas_set_font(canvas, FontSecondary); + snprintf(count_text, sizeof(count_text), count_text_fmt, (int)i2c_scanner->nb_found); + canvas_draw_str_aligned(canvas, 50, 3, AlignLeft, AlignTop, count_text); + uint8_t x_pos = 0; + uint8_t y_pos = 0; + uint8_t idx_to_print = 0; + for(uint8_t i = 0; i < (int)i2c_scanner->nb_found; i++) { + idx_to_print = i + i2c_scanner->menu_index * 3; + if(idx_to_print >= MAX_I2C_ADDR) { + break; + } + snprintf( + count_text, sizeof(count_text), "0x%02x ", (int)i2c_scanner->addresses[idx_to_print]); + if(i < 3) { + x_pos = 50 + (i * 26); + y_pos = 15; + } else if(i < 6) { + x_pos = 50 + ((i - 3) * 26); + y_pos = 25; + } else if(i < 9) { + x_pos = 50 + ((i - 6) * 26); + y_pos = 35; + } else if(i < 12) { + x_pos = 50 + ((i - 9) * 26); + y_pos = 45; + } else if(i < 15) { + x_pos = 50 + ((i - 12) * 26); + y_pos = 55; + } else { + break; + } + canvas_draw_str_aligned(canvas, x_pos, y_pos, AlignLeft, AlignTop, count_text); + } + // Right cursor + y_pos = 14 + i2c_scanner->menu_index; + canvas_draw_rbox(canvas, 125, y_pos, 3, 10, 1); + + // Button + canvas_draw_rbox(canvas, 70, 48, 45, 13, 3); + canvas_set_color(canvas, ColorWhite); + canvas_draw_icon(canvas, 75, 50, &I_Ok_btn_9x9); + canvas_draw_str_aligned(canvas, 85, 51, AlignLeft, AlignTop, "Scan"); +} \ No newline at end of file diff --git a/applications/plugins/flipper_i2ctools/views/scanner_view.h b/applications/plugins/flipper_i2ctools/views/scanner_view.h new file mode 100644 index 000000000..249a12942 --- /dev/null +++ b/applications/plugins/flipper_i2ctools/views/scanner_view.h @@ -0,0 +1,11 @@ +#include +#include +#include + +#include "../i2cscanner.h" + +#define SCAN_MENU_TEXT "Scan" +#define SCAN_MENU_X 75 +#define SCAN_MENU_Y 6 + +void draw_scanner_view(Canvas* canvas, i2cScanner* i2c_scanner); \ No newline at end of file diff --git a/applications/plugins/flipper_i2ctools/views/sender_view.c b/applications/plugins/flipper_i2ctools/views/sender_view.c new file mode 100644 index 000000000..a48e9b5dc --- /dev/null +++ b/applications/plugins/flipper_i2ctools/views/sender_view.c @@ -0,0 +1,52 @@ +#include "sender_view.h" + +void draw_sender_view(Canvas* canvas, i2cSender* i2c_sender) { + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_draw_rframe(canvas, 0, 0, 128, 64, 3); + canvas_draw_icon(canvas, 2, 13, &I_passport_happy2_46x49); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, SEND_MENU_TEXT); + + if(!i2c_sender->scanner->scanned) { + scan_i2c_bus(i2c_sender->scanner); + } + + canvas_set_font(canvas, FontSecondary); + if(i2c_sender->scanner->nb_found <= 0) { + canvas_draw_str_aligned(canvas, 60, 5, AlignLeft, AlignTop, "No peripherals"); + canvas_draw_str_aligned(canvas, 60, 15, AlignLeft, AlignTop, "Found"); + return; + } + canvas_draw_rbox(canvas, 70, 48, 45, 13, 3); + canvas_set_color(canvas, ColorWhite); + canvas_draw_icon(canvas, 75, 50, &I_Ok_btn_9x9); + canvas_draw_str_aligned(canvas, 85, 51, AlignLeft, AlignTop, "Send"); + canvas_set_color(canvas, ColorBlack); + canvas_draw_str_aligned(canvas, 50, 5, AlignLeft, AlignTop, "Addr: "); + canvas_draw_icon(canvas, 80, 5, &I_ButtonLeft_4x7); + canvas_draw_icon(canvas, 115, 5, &I_ButtonRight_4x7); + char addr_text[8]; + snprintf( + addr_text, + sizeof(addr_text), + "0x%02x", + (int)i2c_sender->scanner->addresses[i2c_sender->address_idx]); + canvas_draw_str_aligned(canvas, 90, 5, AlignLeft, AlignTop, addr_text); + canvas_draw_str_aligned(canvas, 50, 15, AlignLeft, AlignTop, "Value: "); + + canvas_draw_icon(canvas, 80, 17, &I_ButtonUp_7x4); + canvas_draw_icon(canvas, 115, 17, &I_ButtonDown_7x4); + snprintf(addr_text, sizeof(addr_text), "0x%02x", (int)i2c_sender->value); + canvas_draw_str_aligned(canvas, 90, 15, AlignLeft, AlignTop, addr_text); + if(i2c_sender->must_send) { + i2c_send(i2c_sender); + } + canvas_draw_str_aligned(canvas, 50, 25, AlignLeft, AlignTop, "Result: "); + if(i2c_sender->sended) { + for(uint8_t i = 0; i < sizeof(i2c_sender->recv); i++) { + snprintf(addr_text, sizeof(addr_text), "0x%02x", (int)i2c_sender->recv[i]); + canvas_draw_str_aligned(canvas, 90, 25 + (i * 10), AlignLeft, AlignTop, addr_text); + } + } +} \ No newline at end of file diff --git a/applications/plugins/flipper_i2ctools/views/sender_view.h b/applications/plugins/flipper_i2ctools/views/sender_view.h new file mode 100644 index 000000000..d0ab28ba4 --- /dev/null +++ b/applications/plugins/flipper_i2ctools/views/sender_view.h @@ -0,0 +1,11 @@ +#include +#include +#include + +#include "../i2csender.h" + +#define SEND_MENU_TEXT "Send" +#define SEND_MENU_X 75 +#define SEND_MENU_Y 34 + +void draw_sender_view(Canvas* canvas, i2cSender* i2c_sender); \ No newline at end of file diff --git a/applications/plugins/flipper_i2ctools/views/sniffer_view.c b/applications/plugins/flipper_i2ctools/views/sniffer_view.c new file mode 100644 index 000000000..8f289066c --- /dev/null +++ b/applications/plugins/flipper_i2ctools/views/sniffer_view.c @@ -0,0 +1,62 @@ +#include "sniffer_view.h" + +void draw_sniffer_view(Canvas* canvas, i2cSniffer* i2c_sniffer) { + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_draw_rframe(canvas, 0, 0, 128, 64, 3); + canvas_draw_icon(canvas, 2, 13, &I_passport_happy2_46x49); + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, SNIFF_MENU_TEXT); + canvas_set_font(canvas, FontSecondary); + + // Button + canvas_draw_rbox(canvas, 70, 48, 45, 13, 3); + canvas_set_color(canvas, ColorWhite); + canvas_draw_icon(canvas, 75, 50, &I_Ok_btn_9x9); + if(!i2c_sniffer->started) { + canvas_draw_str_aligned(canvas, 85, 51, AlignLeft, AlignTop, "Start"); + } else { + canvas_draw_str_aligned(canvas, 85, 51, AlignLeft, AlignTop, "Stop"); + } + canvas_set_color(canvas, ColorBlack); + // Address text + char addr_text[8]; + snprintf( + addr_text, + sizeof(addr_text), + "0x%02x", + (int)(i2c_sniffer->frames[i2c_sniffer->menu_index].data[0] >> 1)); + canvas_draw_str_aligned(canvas, 50, 3, AlignLeft, AlignTop, "Addr: "); + canvas_draw_str_aligned(canvas, 75, 3, AlignLeft, AlignTop, addr_text); + // R/W + if((int)(i2c_sniffer->frames[i2c_sniffer->menu_index].data[0]) % 2 == 0) { + canvas_draw_str_aligned(canvas, 105, 3, AlignLeft, AlignTop, "W"); + } else { + canvas_draw_str_aligned(canvas, 105, 3, AlignLeft, AlignTop, "R"); + } + // nbFrame text + canvas_draw_str_aligned(canvas, 50, 13, AlignLeft, AlignTop, "Frames: "); + snprintf(addr_text, sizeof(addr_text), "%d", (int)i2c_sniffer->menu_index + 1); + canvas_draw_str_aligned(canvas, 90, 13, AlignLeft, AlignTop, addr_text); + canvas_draw_str_aligned(canvas, 100, 13, AlignLeft, AlignTop, "/"); + snprintf(addr_text, sizeof(addr_text), "%d", (int)i2c_sniffer->frame_index + 1); + canvas_draw_str_aligned(canvas, 110, 13, AlignLeft, AlignTop, addr_text); + // Frames content + uint8_t x_pos = 0; + uint8_t y_pos = 23; + for(uint8_t i = 1; i < i2c_sniffer->frames[i2c_sniffer->menu_index].data_index; i++) { + snprintf( + addr_text, + sizeof(addr_text), + "0x%02x", + (int)i2c_sniffer->frames[i2c_sniffer->menu_index].data[i]); + x_pos = 50 + (i - 1) * 35; + canvas_draw_str_aligned(canvas, x_pos, y_pos, AlignLeft, AlignTop, addr_text); + if(i2c_sniffer->frames[i2c_sniffer->menu_index].ack[i]) { + canvas_draw_str_aligned(canvas, x_pos + 24, y_pos, AlignLeft, AlignTop, "A"); + } else { + canvas_draw_str_aligned(canvas, x_pos + 24, y_pos, AlignLeft, AlignTop, "N"); + } + } +} \ No newline at end of file diff --git a/applications/plugins/flipper_i2ctools/views/sniffer_view.h b/applications/plugins/flipper_i2ctools/views/sniffer_view.h new file mode 100644 index 000000000..da46aaef1 --- /dev/null +++ b/applications/plugins/flipper_i2ctools/views/sniffer_view.h @@ -0,0 +1,11 @@ +#include +#include +#include + +#include "../i2csniffer.h" + +#define SNIFF_MENU_TEXT "Sniff" +#define SNIFF_MENU_X 75 +#define SNIFF_MENU_Y 20 + +void draw_sniffer_view(Canvas* canvas, i2cSniffer* i2c_sniffer); \ No newline at end of file From 93a6e17ce57222fa7b92a930dd4f9aaee3a146bf Mon Sep 17 00:00:00 2001 From: gornekich Date: Fri, 28 Oct 2022 20:10:16 +0400 Subject: [PATCH 08/14] [FL-2933] Mf Classic initial write, update, detect reader (#1941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * nfc: introduce nfc write * nfc: add write logic * nfc worker: add write state * nfc: add mfc update logic * nfc: add update success logic * nfc: add custom card for detect reader * nfc: update write logic * nfc: add halt command, add notifications * nfc: add write fail scene * nfc: fixes and clean up * nfc: fix navigation ad notifications * nfc: fix detect reader nfc data setter Co-authored-by: あく --- .../main/nfc/scenes/nfc_scene_config.h | 6 + .../main/nfc/scenes/nfc_scene_detect_reader.c | 5 + .../nfc/scenes/nfc_scene_mf_classic_update.c | 98 +++++++ .../nfc_scene_mf_classic_update_success.c | 44 +++ .../nfc/scenes/nfc_scene_mf_classic_write.c | 92 ++++++ .../scenes/nfc_scene_mf_classic_write_fail.c | 58 ++++ .../nfc_scene_mf_classic_write_success.c | 44 +++ .../scenes/nfc_scene_mf_classic_wrong_card.c | 53 ++++ .../main/nfc/scenes/nfc_scene_saved_menu.c | 34 +++ .../main/nfc/scenes/nfc_scene_start.c | 1 + applications/main/nfc/views/detect_reader.c | 36 +++ applications/main/nfc/views/detect_reader.h | 2 + firmware/targets/f7/furi_hal/furi_hal_nfc.c | 10 +- lib/nfc/helpers/reader_analyzer.c | 9 +- lib/nfc/helpers/reader_analyzer.h | 2 + lib/nfc/nfc_device.c | 7 + lib/nfc/nfc_worker.c | 150 +++++++++- lib/nfc/nfc_worker.h | 7 +- lib/nfc/nfc_worker_i.h | 4 + lib/nfc/protocols/crypto1.c | 52 ++++ lib/nfc/protocols/crypto1.h | 14 + lib/nfc/protocols/mifare_classic.c | 271 ++++++++++++------ lib/nfc/protocols/mifare_classic.h | 43 +++ 23 files changed, 949 insertions(+), 93 deletions(-) create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_update.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_update_success.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_write.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_write_fail.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_write_success.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_wrong_card.c diff --git a/applications/main/nfc/scenes/nfc_scene_config.h b/applications/main/nfc/scenes/nfc_scene_config.h index a25850c84..9b922add2 100644 --- a/applications/main/nfc/scenes/nfc_scene_config.h +++ b/applications/main/nfc/scenes/nfc_scene_config.h @@ -36,6 +36,12 @@ ADD_SCENE(nfc, mf_classic_keys_list, MfClassicKeysList) ADD_SCENE(nfc, mf_classic_keys_delete, MfClassicKeysDelete) ADD_SCENE(nfc, mf_classic_keys_warn_duplicate, MfClassicKeysWarnDuplicate) ADD_SCENE(nfc, mf_classic_dict_attack, MfClassicDictAttack) +ADD_SCENE(nfc, mf_classic_write, MfClassicWrite) +ADD_SCENE(nfc, mf_classic_write_success, MfClassicWriteSuccess) +ADD_SCENE(nfc, mf_classic_write_fail, MfClassicWriteFail) +ADD_SCENE(nfc, mf_classic_update, MfClassicUpdate) +ADD_SCENE(nfc, mf_classic_update_success, MfClassicUpdateSuccess) +ADD_SCENE(nfc, mf_classic_wrong_card, MfClassicWrongCard) ADD_SCENE(nfc, emv_read_success, EmvReadSuccess) ADD_SCENE(nfc, emv_menu, EmvMenu) ADD_SCENE(nfc, emulate_apdu_sequence, EmulateApduSequence) diff --git a/applications/main/nfc/scenes/nfc_scene_detect_reader.c b/applications/main/nfc/scenes/nfc_scene_detect_reader.c index abf1437d2..745946157 100644 --- a/applications/main/nfc/scenes/nfc_scene_detect_reader.c +++ b/applications/main/nfc/scenes/nfc_scene_detect_reader.c @@ -28,6 +28,11 @@ void nfc_scene_detect_reader_on_enter(void* context) { detect_reader_set_callback(nfc->detect_reader, nfc_scene_detect_reader_callback, nfc); detect_reader_set_nonces_max(nfc->detect_reader, NFC_SCENE_DETECT_READER_PAIR_NONCES_MAX); + NfcDeviceData* dev_data = &nfc->dev->dev_data; + if(dev_data->nfc_data.uid_len) { + detect_reader_set_uid( + nfc->detect_reader, dev_data->nfc_data.uid, dev_data->nfc_data.uid_len); + } // Store number of collected nonces in scene state scene_manager_set_scene_state(nfc->scene_manager, NfcSceneDetectReader, 0); diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_update.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_update.c new file mode 100644 index 000000000..dd3a6f7d5 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_update.c @@ -0,0 +1,98 @@ +#include "../nfc_i.h" +#include + +enum { + NfcSceneMfClassicUpdateStateCardSearch, + NfcSceneMfClassicUpdateStateCardFound, +}; + +bool nfc_mf_classic_update_worker_callback(NfcWorkerEvent event, void* context) { + furi_assert(context); + + Nfc* nfc = context; + view_dispatcher_send_custom_event(nfc->view_dispatcher, event); + + return true; +} + +static void nfc_scene_mf_classic_update_setup_view(Nfc* nfc) { + Popup* popup = nfc->popup; + popup_reset(popup); + uint32_t state = scene_manager_get_scene_state(nfc->scene_manager, NfcSceneMfClassicUpdate); + + if(state == NfcSceneMfClassicUpdateStateCardSearch) { + popup_set_text( + nfc->popup, "Apply the initial\ncard only", 128, 32, AlignRight, AlignCenter); + popup_set_icon(nfc->popup, 0, 8, &I_NFC_manual_60x50); + } else { + popup_set_header(popup, "Updating\nDon't move...", 52, 32, AlignLeft, AlignCenter); + popup_set_icon(popup, 12, 23, &A_Loading_24); + } + + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup); +} + +void nfc_scene_mf_classic_update_on_enter(void* context) { + Nfc* nfc = context; + DOLPHIN_DEED(DolphinDeedNfcEmulate); + + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicUpdate, NfcSceneMfClassicUpdateStateCardSearch); + nfc_scene_mf_classic_update_setup_view(nfc); + + // Setup and start worker + nfc_worker_start( + nfc->worker, + NfcWorkerStateMfClassicUpdate, + &nfc->dev->dev_data, + nfc_mf_classic_update_worker_callback, + nfc); + nfc_blink_emulate_start(nfc); +} + +bool nfc_scene_mf_classic_update_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcWorkerEventSuccess) { + nfc_worker_stop(nfc->worker); + if(nfc_device_save_shadow(nfc->dev, nfc->dev->dev_name)) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicUpdateSuccess); + } else { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWrongCard); + } + consumed = true; + } else if(event.event == NfcWorkerEventWrongCard) { + nfc_worker_stop(nfc->worker); + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWrongCard); + consumed = true; + } else if(event.event == NfcWorkerEventCardDetected) { + scene_manager_set_scene_state( + nfc->scene_manager, + NfcSceneMfClassicUpdate, + NfcSceneMfClassicUpdateStateCardFound); + nfc_scene_mf_classic_update_setup_view(nfc); + consumed = true; + } else if(event.event == NfcWorkerEventNoCardDetected) { + scene_manager_set_scene_state( + nfc->scene_manager, + NfcSceneMfClassicUpdate, + NfcSceneMfClassicUpdateStateCardSearch); + nfc_scene_mf_classic_update_setup_view(nfc); + consumed = true; + } + } + return consumed; +} + +void nfc_scene_mf_classic_update_on_exit(void* context) { + Nfc* nfc = context; + nfc_worker_stop(nfc->worker); + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicUpdate, NfcSceneMfClassicUpdateStateCardSearch); + // Clear view + popup_reset(nfc->popup); + + nfc_blink_stop(nfc); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_update_success.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_update_success.c new file mode 100644 index 000000000..fef8fd5e9 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_update_success.c @@ -0,0 +1,44 @@ +#include "../nfc_i.h" +#include + +void nfc_scene_mf_classic_update_success_popup_callback(void* context) { + Nfc* nfc = context; + view_dispatcher_send_custom_event(nfc->view_dispatcher, NfcCustomEventViewExit); +} + +void nfc_scene_mf_classic_update_success_on_enter(void* context) { + Nfc* nfc = context; + DOLPHIN_DEED(DolphinDeedNfcSave); + + notification_message(nfc->notifications, &sequence_success); + + Popup* popup = nfc->popup; + popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59); + popup_set_header(popup, "Updated!", 11, 20, AlignLeft, AlignBottom); + popup_set_timeout(popup, 1500); + popup_set_context(popup, nfc); + popup_set_callback(popup, nfc_scene_mf_classic_update_success_popup_callback); + popup_enable_timeout(popup); + + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup); +} + +bool nfc_scene_mf_classic_update_success_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcCustomEventViewExit) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneFileSelect); + } + } + return consumed; +} + +void nfc_scene_mf_classic_update_success_on_exit(void* context) { + Nfc* nfc = context; + + // Clear view + popup_reset(nfc->popup); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_write.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_write.c new file mode 100644 index 000000000..3543cbc58 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_write.c @@ -0,0 +1,92 @@ +#include "../nfc_i.h" +#include + +enum { + NfcSceneMfClassicWriteStateCardSearch, + NfcSceneMfClassicWriteStateCardFound, +}; + +bool nfc_mf_classic_write_worker_callback(NfcWorkerEvent event, void* context) { + furi_assert(context); + + Nfc* nfc = context; + view_dispatcher_send_custom_event(nfc->view_dispatcher, event); + + return true; +} + +static void nfc_scene_mf_classic_write_setup_view(Nfc* nfc) { + Popup* popup = nfc->popup; + popup_reset(popup); + uint32_t state = scene_manager_get_scene_state(nfc->scene_manager, NfcSceneMfClassicWrite); + + if(state == NfcSceneMfClassicWriteStateCardSearch) { + popup_set_text( + nfc->popup, "Apply the initial\ncard only", 128, 32, AlignRight, AlignCenter); + popup_set_icon(nfc->popup, 0, 8, &I_NFC_manual_60x50); + } else { + popup_set_header(popup, "Writing\nDon't move...", 52, 32, AlignLeft, AlignCenter); + popup_set_icon(popup, 12, 23, &A_Loading_24); + } + + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup); +} + +void nfc_scene_mf_classic_write_on_enter(void* context) { + Nfc* nfc = context; + DOLPHIN_DEED(DolphinDeedNfcEmulate); + + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicWrite, NfcSceneMfClassicWriteStateCardSearch); + nfc_scene_mf_classic_write_setup_view(nfc); + + // Setup and start worker + nfc_worker_start( + nfc->worker, + NfcWorkerStateMfClassicWrite, + &nfc->dev->dev_data, + nfc_mf_classic_write_worker_callback, + nfc); + nfc_blink_emulate_start(nfc); +} + +bool nfc_scene_mf_classic_write_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcWorkerEventSuccess) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWriteSuccess); + consumed = true; + } else if(event.event == NfcWorkerEventFail) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWriteFail); + consumed = true; + } else if(event.event == NfcWorkerEventWrongCard) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWrongCard); + consumed = true; + } else if(event.event == NfcWorkerEventCardDetected) { + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicWrite, NfcSceneMfClassicWriteStateCardFound); + nfc_scene_mf_classic_write_setup_view(nfc); + consumed = true; + } else if(event.event == NfcWorkerEventNoCardDetected) { + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicWrite, NfcSceneMfClassicWriteStateCardSearch); + nfc_scene_mf_classic_write_setup_view(nfc); + consumed = true; + } + } + return consumed; +} + +void nfc_scene_mf_classic_write_on_exit(void* context) { + Nfc* nfc = context; + + nfc_worker_stop(nfc->worker); + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicWrite, NfcSceneMfClassicWriteStateCardSearch); + // Clear view + popup_reset(nfc->popup); + + nfc_blink_stop(nfc); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_write_fail.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_write_fail.c new file mode 100644 index 000000000..aeea6eef0 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_write_fail.c @@ -0,0 +1,58 @@ +#include "../nfc_i.h" + +void nfc_scene_mf_classic_write_fail_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + Nfc* nfc = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(nfc->view_dispatcher, result); + } +} + +void nfc_scene_mf_classic_write_fail_on_enter(void* context) { + Nfc* nfc = context; + Widget* widget = nfc->widget; + + notification_message(nfc->notifications, &sequence_error); + + widget_add_icon_element(widget, 72, 17, &I_DolphinCommon_56x48); + widget_add_string_element( + widget, 7, 4, AlignLeft, AlignTop, FontPrimary, "Writing gone wrong!"); + widget_add_string_multiline_element( + widget, + 7, + 17, + AlignLeft, + AlignTop, + FontSecondary, + "Not all sectors\nwere written\ncorrectly."); + + widget_add_button_element( + widget, GuiButtonTypeLeft, "Finish", nfc_scene_mf_classic_write_fail_widget_callback, nfc); + + // Setup and start worker + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewWidget); +} + +bool nfc_scene_mf_classic_write_fail_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneFileSelect); + } + } else if(event.type == SceneManagerEventTypeBack) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneSavedMenu); + } + return consumed; +} + +void nfc_scene_mf_classic_write_fail_on_exit(void* context) { + Nfc* nfc = context; + + widget_reset(nfc->widget); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_write_success.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_write_success.c new file mode 100644 index 000000000..2f2a3beb1 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_write_success.c @@ -0,0 +1,44 @@ +#include "../nfc_i.h" +#include + +void nfc_scene_mf_classic_write_success_popup_callback(void* context) { + Nfc* nfc = context; + view_dispatcher_send_custom_event(nfc->view_dispatcher, NfcCustomEventViewExit); +} + +void nfc_scene_mf_classic_write_success_on_enter(void* context) { + Nfc* nfc = context; + DOLPHIN_DEED(DolphinDeedNfcSave); + + notification_message(nfc->notifications, &sequence_success); + + Popup* popup = nfc->popup; + popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59); + popup_set_header(popup, "Successfully\nwritten", 13, 22, AlignLeft, AlignBottom); + popup_set_timeout(popup, 1500); + popup_set_context(popup, nfc); + popup_set_callback(popup, nfc_scene_mf_classic_write_success_popup_callback); + popup_enable_timeout(popup); + + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup); +} + +bool nfc_scene_mf_classic_write_success_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcCustomEventViewExit) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneFileSelect); + } + } + return consumed; +} + +void nfc_scene_mf_classic_write_success_on_exit(void* context) { + Nfc* nfc = context; + + // Clear view + popup_reset(nfc->popup); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_wrong_card.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_wrong_card.c new file mode 100644 index 000000000..2c56270e3 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_wrong_card.c @@ -0,0 +1,53 @@ +#include "../nfc_i.h" + +void nfc_scene_mf_classic_wrong_card_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + Nfc* nfc = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(nfc->view_dispatcher, result); + } +} + +void nfc_scene_mf_classic_wrong_card_on_enter(void* context) { + Nfc* nfc = context; + Widget* widget = nfc->widget; + + notification_message(nfc->notifications, &sequence_error); + + widget_add_icon_element(widget, 73, 17, &I_DolphinCommon_56x48); + widget_add_string_element( + widget, 3, 4, AlignLeft, AlignTop, FontPrimary, "This is wrong card"); + widget_add_string_multiline_element( + widget, + 4, + 17, + AlignLeft, + AlignTop, + FontSecondary, + "Data management\nis only possible\nwith initial card"); + widget_add_button_element( + widget, GuiButtonTypeLeft, "Retry", nfc_scene_mf_classic_wrong_card_widget_callback, nfc); + + // Setup and start worker + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewWidget); +} + +bool nfc_scene_mf_classic_wrong_card_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_previous_scene(nfc->scene_manager); + } + } + return consumed; +} + +void nfc_scene_mf_classic_wrong_card_on_exit(void* context) { + Nfc* nfc = context; + + widget_reset(nfc->widget); +} \ No newline at end of file diff --git a/applications/main/nfc/scenes/nfc_scene_saved_menu.c b/applications/main/nfc/scenes/nfc_scene_saved_menu.c index 09d2c2d61..231f12089 100644 --- a/applications/main/nfc/scenes/nfc_scene_saved_menu.c +++ b/applications/main/nfc/scenes/nfc_scene_saved_menu.c @@ -4,6 +4,9 @@ enum SubmenuIndex { SubmenuIndexEmulate, SubmenuIndexEditUid, + SubmenuIndexDetectReader, + SubmenuIndexWrite, + SubmenuIndexUpdate, SubmenuIndexRename, SubmenuIndexDelete, SubmenuIndexInfo, @@ -42,6 +45,28 @@ void nfc_scene_saved_menu_on_enter(void* context) { submenu_add_item( submenu, "Emulate", SubmenuIndexEmulate, nfc_scene_saved_menu_submenu_callback, nfc); } + if(nfc->dev->format == NfcDeviceSaveFormatMifareClassic) { + if(!mf_classic_is_card_read(&nfc->dev->dev_data.mf_classic_data)) { + submenu_add_item( + submenu, + "Detect reader", + SubmenuIndexDetectReader, + nfc_scene_saved_menu_submenu_callback, + nfc); + } + submenu_add_item( + submenu, + "Write To Initial Card", + SubmenuIndexWrite, + nfc_scene_saved_menu_submenu_callback, + nfc); + submenu_add_item( + submenu, + "Update From Initial Card", + SubmenuIndexUpdate, + nfc_scene_saved_menu_submenu_callback, + nfc); + } submenu_add_item( submenu, "Info", SubmenuIndexInfo, nfc_scene_saved_menu_submenu_callback, nfc); if(nfc->dev->shadow_file_exist) { @@ -79,6 +104,15 @@ bool nfc_scene_saved_menu_on_event(void* context, SceneManagerEvent event) { } DOLPHIN_DEED(DolphinDeedNfcEmulate); consumed = true; + } else if(event.event == SubmenuIndexDetectReader) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneDetectReader); + consumed = true; + } else if(event.event == SubmenuIndexWrite) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWrite); + consumed = true; + } else if(event.event == SubmenuIndexUpdate) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicUpdate); + consumed = true; } else if(event.event == SubmenuIndexRename) { scene_manager_next_scene(nfc->scene_manager, NfcSceneSaveName); consumed = true; diff --git a/applications/main/nfc/scenes/nfc_scene_start.c b/applications/main/nfc/scenes/nfc_scene_start.c index 0c4ec1cf9..028f85ae0 100644 --- a/applications/main/nfc/scenes/nfc_scene_start.c +++ b/applications/main/nfc/scenes/nfc_scene_start.c @@ -53,6 +53,7 @@ bool nfc_scene_start_on_event(void* context, SceneManagerEvent event) { } else if(event.event == SubmenuIndexDetectReader) { bool sd_exist = storage_sd_status(nfc->dev->storage) == FSE_OK; if(sd_exist) { + nfc_device_data_clear(&nfc->dev->dev_data); scene_manager_next_scene(nfc->scene_manager, NfcSceneDetectReader); DOLPHIN_DEED(DolphinDeedNfcDetectReader); } else { diff --git a/applications/main/nfc/views/detect_reader.c b/applications/main/nfc/views/detect_reader.c index 91537868b..e5951beb2 100644 --- a/applications/main/nfc/views/detect_reader.c +++ b/applications/main/nfc/views/detect_reader.c @@ -2,6 +2,8 @@ #include #include +#define DETECT_READER_UID_MAX_LEN (10) + struct DetectReader { View* view; DetectReaderDoneCallback callback; @@ -12,6 +14,7 @@ typedef struct { uint16_t nonces; uint16_t nonces_max; DetectReaderState state; + FuriString* uid_str; } DetectReaderViewModel; static void detect_reader_draw_callback(Canvas* canvas, void* model) { @@ -23,6 +26,10 @@ static void detect_reader_draw_callback(Canvas* canvas, void* model) { if(m->state == DetectReaderStateStart) { snprintf(text, sizeof(text), "Touch the reader"); canvas_draw_icon(canvas, 21, 13, &I_Move_flipper_26x39); + if(furi_string_size(m->uid_str)) { + elements_multiline_text_aligned( + canvas, 64, 64, AlignCenter, AlignBottom, furi_string_get_cstr(m->uid_str)); + } } else if(m->state == DetectReaderStateReaderDetected) { snprintf(text, sizeof(text), "Move the Flipper away"); canvas_draw_icon(canvas, 24, 25, &I_Release_arrow_18x15); @@ -86,12 +93,24 @@ DetectReader* detect_reader_alloc() { view_set_input_callback(detect_reader->view, detect_reader_input_callback); view_set_context(detect_reader->view, detect_reader); + with_view_model( + detect_reader->view, + DetectReaderViewModel * model, + { model->uid_str = furi_string_alloc(); }, + false); + return detect_reader; } void detect_reader_free(DetectReader* detect_reader) { furi_assert(detect_reader); + with_view_model( + detect_reader->view, + DetectReaderViewModel * model, + { furi_string_free(model->uid_str); }, + false); + view_free(detect_reader->view); free(detect_reader); } @@ -106,6 +125,7 @@ void detect_reader_reset(DetectReader* detect_reader) { model->nonces = 0; model->nonces_max = 0; model->state = DetectReaderStateStart; + furi_string_reset(model->uid_str); }, false); } @@ -152,3 +172,19 @@ void detect_reader_set_state(DetectReader* detect_reader, DetectReaderState stat with_view_model( detect_reader->view, DetectReaderViewModel * model, { model->state = state; }, true); } + +void detect_reader_set_uid(DetectReader* detect_reader, uint8_t* uid, uint8_t uid_len) { + furi_assert(detect_reader); + furi_assert(uid); + furi_assert(uid_len < DETECT_READER_UID_MAX_LEN); + with_view_model( + detect_reader->view, + DetectReaderViewModel * model, + { + furi_string_set_str(model->uid_str, "UID:"); + for(size_t i = 0; i < uid_len; i++) { + furi_string_cat_printf(model->uid_str, " %02X", uid[i]); + } + }, + true); +} diff --git a/applications/main/nfc/views/detect_reader.h b/applications/main/nfc/views/detect_reader.h index aabdd7c87..6481216b4 100644 --- a/applications/main/nfc/views/detect_reader.h +++ b/applications/main/nfc/views/detect_reader.h @@ -32,3 +32,5 @@ void detect_reader_set_nonces_max(DetectReader* detect_reader, uint16_t nonces_m void detect_reader_set_nonces_collected(DetectReader* detect_reader, uint16_t nonces_collected); void detect_reader_set_state(DetectReader* detect_reader, DetectReaderState state); + +void detect_reader_set_uid(DetectReader* detect_reader, uint8_t* uid, uint8_t uid_len); diff --git a/firmware/targets/f7/furi_hal/furi_hal_nfc.c b/firmware/targets/f7/furi_hal/furi_hal_nfc.c index 069ac4ea4..3ebf4f82b 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_nfc.c +++ b/firmware/targets/f7/furi_hal/furi_hal_nfc.c @@ -620,6 +620,10 @@ uint16_t furi_hal_nfc_bitstream_to_data_and_parity( uint16_t in_buff_bits, uint8_t* out_data, uint8_t* out_parity) { + if(in_buff_bits < 8) { + out_data[0] = in_buff[0]; + return in_buff_bits; + } if(in_buff_bits % 9 != 0) { return 0; } @@ -635,7 +639,7 @@ uint16_t furi_hal_nfc_bitstream_to_data_and_parity( bit_processed += 9; curr_byte++; } - return curr_byte; + return curr_byte * 8; } bool furi_hal_nfc_tx_rx(FuriHalNfcTxRxContext* tx_rx, uint16_t timeout_ms) { @@ -692,8 +696,8 @@ bool furi_hal_nfc_tx_rx(FuriHalNfcTxRxContext* tx_rx, uint16_t timeout_ms) { if(tx_rx->tx_rx_type == FuriHalNfcTxRxTypeRaw || tx_rx->tx_rx_type == FuriHalNfcTxRxTypeRxRaw) { - tx_rx->rx_bits = 8 * furi_hal_nfc_bitstream_to_data_and_parity( - temp_rx_buff, *temp_rx_bits, tx_rx->rx_data, tx_rx->rx_parity); + tx_rx->rx_bits = furi_hal_nfc_bitstream_to_data_and_parity( + temp_rx_buff, *temp_rx_bits, tx_rx->rx_data, tx_rx->rx_parity); } else { memcpy(tx_rx->rx_data, temp_rx_buff, MIN(*temp_rx_bits / 8, FURI_HAL_NFC_DATA_BUFF_SIZE)); tx_rx->rx_bits = *temp_rx_bits; diff --git a/lib/nfc/helpers/reader_analyzer.c b/lib/nfc/helpers/reader_analyzer.c index 0ba657a2e..7fed9c6f6 100644 --- a/lib/nfc/helpers/reader_analyzer.c +++ b/lib/nfc/helpers/reader_analyzer.c @@ -201,10 +201,17 @@ NfcProtocol FuriHalNfcDevData* reader_analyzer_get_nfc_data(ReaderAnalyzer* instance) { furi_assert(instance); - + instance->nfc_data = reader_analyzer_nfc_data[ReaderAnalyzerNfcDataMfClassic]; return &instance->nfc_data; } +void reader_analyzer_set_nfc_data(ReaderAnalyzer* instance, FuriHalNfcDevData* nfc_data) { + furi_assert(instance); + furi_assert(nfc_data); + + memcpy(&instance->nfc_data, nfc_data, sizeof(FuriHalNfcDevData)); +} + static void reader_analyzer_write( ReaderAnalyzer* instance, uint8_t* data, diff --git a/lib/nfc/helpers/reader_analyzer.h b/lib/nfc/helpers/reader_analyzer.h index cc501f5a6..13bf4d77c 100644 --- a/lib/nfc/helpers/reader_analyzer.h +++ b/lib/nfc/helpers/reader_analyzer.h @@ -35,6 +35,8 @@ NfcProtocol FuriHalNfcDevData* reader_analyzer_get_nfc_data(ReaderAnalyzer* instance); +void reader_analyzer_set_nfc_data(ReaderAnalyzer* instance, FuriHalNfcDevData* nfc_data); + void reader_analyzer_prepare_tx_rx( ReaderAnalyzer* instance, FuriHalNfcTxRxContext* tx_rx, diff --git a/lib/nfc/nfc_device.c b/lib/nfc/nfc_device.c index 740cfae5e..a5e3fc14f 100644 --- a/lib/nfc/nfc_device.c +++ b/lib/nfc/nfc_device.c @@ -1122,6 +1122,13 @@ static bool nfc_device_load_data(NfcDevice* dev, FuriString* path, bool show_dia if(!flipper_format_read_hex(file, "UID", data->uid, data->uid_len)) break; if(!flipper_format_read_hex(file, "ATQA", data->atqa, 2)) break; if(!flipper_format_read_hex(file, "SAK", &data->sak, 1)) break; + // Load CUID + uint8_t* cuid_start = data->uid; + if(data->uid_len == 7) { + cuid_start = &data->uid[3]; + } + data->cuid = (cuid_start[0] << 24) | (cuid_start[1] << 16) | (cuid_start[2] << 8) | + (cuid_start[3]); // Parse other data if(dev->format == NfcDeviceSaveFormatMifareUl) { if(!nfc_device_load_mifare_ul_data(file, dev)) break; diff --git a/lib/nfc/nfc_worker.c b/lib/nfc/nfc_worker.c index ebe203905..e1e379a06 100644 --- a/lib/nfc/nfc_worker.c +++ b/lib/nfc/nfc_worker.c @@ -99,6 +99,10 @@ int32_t nfc_worker_task(void* context) { nfc_worker_emulate_mf_ultralight(nfc_worker); } else if(nfc_worker->state == NfcWorkerStateMfClassicEmulate) { nfc_worker_emulate_mf_classic(nfc_worker); + } else if(nfc_worker->state == NfcWorkerStateMfClassicWrite) { + nfc_worker_write_mf_classic(nfc_worker); + } else if(nfc_worker->state == NfcWorkerStateMfClassicUpdate) { + nfc_worker_update_mf_classic(nfc_worker); } else if(nfc_worker->state == NfcWorkerStateReadMfUltralightReadAuth) { nfc_worker_mf_ultralight_read_auth(nfc_worker); } else if(nfc_worker->state == NfcWorkerStateMfClassicDictAttack) { @@ -666,6 +670,144 @@ void nfc_worker_emulate_mf_classic(NfcWorker* nfc_worker) { rfal_platform_spi_release(); } +void nfc_worker_write_mf_classic(NfcWorker* nfc_worker) { + FuriHalNfcTxRxContext tx_rx = {}; + bool card_found_notified = false; + FuriHalNfcDevData nfc_data = {}; + MfClassicData* src_data = &nfc_worker->dev_data->mf_classic_data; + MfClassicData dest_data = *src_data; + + while(nfc_worker->state == NfcWorkerStateMfClassicWrite) { + if(furi_hal_nfc_detect(&nfc_data, 200)) { + if(!card_found_notified) { + nfc_worker->callback(NfcWorkerEventCardDetected, nfc_worker->context); + card_found_notified = true; + } + furi_hal_nfc_sleep(); + + FURI_LOG_I(TAG, "Check low level nfc data"); + if(memcmp(&nfc_data, &nfc_worker->dev_data->nfc_data, sizeof(FuriHalNfcDevData))) { + FURI_LOG_E(TAG, "Wrong card"); + nfc_worker->callback(NfcWorkerEventWrongCard, nfc_worker->context); + break; + } + + FURI_LOG_I(TAG, "Check mf classic type"); + MfClassicType type = + mf_classic_get_classic_type(nfc_data.atqa[0], nfc_data.atqa[1], nfc_data.sak); + if(type != nfc_worker->dev_data->mf_classic_data.type) { + FURI_LOG_E(TAG, "Wrong mf classic type"); + nfc_worker->callback(NfcWorkerEventWrongCard, nfc_worker->context); + break; + } + + // Set blocks not read + mf_classic_set_sector_data_not_read(&dest_data); + FURI_LOG_I(TAG, "Updating card sectors"); + uint8_t total_sectors = mf_classic_get_total_sectors_num(type); + bool write_success = true; + for(uint8_t i = 0; i < total_sectors; i++) { + FURI_LOG_I(TAG, "Reading sector %d", i); + mf_classic_read_sector(&tx_rx, &dest_data, i); + bool old_data_read = mf_classic_is_sector_data_read(src_data, i); + bool new_data_read = mf_classic_is_sector_data_read(&dest_data, i); + if(old_data_read != new_data_read) { + FURI_LOG_E(TAG, "Failed to update sector %d", i); + write_success = false; + break; + } + if(nfc_worker->state != NfcWorkerStateMfClassicWrite) break; + if(!mf_classic_write_sector(&tx_rx, &dest_data, src_data, i)) { + FURI_LOG_E(TAG, "Failed to write %d sector", i); + write_success = false; + break; + } + } + if(nfc_worker->state != NfcWorkerStateMfClassicWrite) break; + if(write_success) { + nfc_worker->callback(NfcWorkerEventSuccess, nfc_worker->context); + break; + } else { + nfc_worker->callback(NfcWorkerEventFail, nfc_worker->context); + break; + } + + } else { + if(card_found_notified) { + nfc_worker->callback(NfcWorkerEventNoCardDetected, nfc_worker->context); + card_found_notified = false; + } + } + furi_delay_ms(300); + } +} + +void nfc_worker_update_mf_classic(NfcWorker* nfc_worker) { + FuriHalNfcTxRxContext tx_rx = {}; + bool card_found_notified = false; + FuriHalNfcDevData nfc_data = {}; + MfClassicData* old_data = &nfc_worker->dev_data->mf_classic_data; + MfClassicData new_data = *old_data; + + while(nfc_worker->state == NfcWorkerStateMfClassicUpdate) { + if(furi_hal_nfc_detect(&nfc_data, 200)) { + if(!card_found_notified) { + nfc_worker->callback(NfcWorkerEventCardDetected, nfc_worker->context); + card_found_notified = true; + } + furi_hal_nfc_sleep(); + + FURI_LOG_I(TAG, "Check low level nfc data"); + if(memcmp(&nfc_data, &nfc_worker->dev_data->nfc_data, sizeof(FuriHalNfcDevData))) { + FURI_LOG_E(TAG, "Low level nfc data mismatch"); + nfc_worker->callback(NfcWorkerEventWrongCard, nfc_worker->context); + break; + } + + FURI_LOG_I(TAG, "Check MF classic type"); + MfClassicType type = + mf_classic_get_classic_type(nfc_data.atqa[0], nfc_data.atqa[1], nfc_data.sak); + if(type != nfc_worker->dev_data->mf_classic_data.type) { + FURI_LOG_E(TAG, "MF classic type mismatch"); + nfc_worker->callback(NfcWorkerEventWrongCard, nfc_worker->context); + break; + } + + // Set blocks not read + mf_classic_set_sector_data_not_read(&new_data); + FURI_LOG_I(TAG, "Updating card sectors"); + uint8_t total_sectors = mf_classic_get_total_sectors_num(type); + bool update_success = true; + for(uint8_t i = 0; i < total_sectors; i++) { + FURI_LOG_I(TAG, "Reading sector %d", i); + mf_classic_read_sector(&tx_rx, &new_data, i); + bool old_data_read = mf_classic_is_sector_data_read(old_data, i); + bool new_data_read = mf_classic_is_sector_data_read(&new_data, i); + if(old_data_read != new_data_read) { + FURI_LOG_E(TAG, "Failed to update sector %d", i); + update_success = false; + break; + } + if(nfc_worker->state != NfcWorkerStateMfClassicUpdate) break; + } + if(nfc_worker->state != NfcWorkerStateMfClassicUpdate) break; + + // Check updated data + if(update_success) { + *old_data = new_data; + nfc_worker->callback(NfcWorkerEventSuccess, nfc_worker->context); + break; + } + } else { + if(card_found_notified) { + nfc_worker->callback(NfcWorkerEventNoCardDetected, nfc_worker->context); + card_found_notified = false; + } + } + furi_delay_ms(300); + } +} + void nfc_worker_mf_ultralight_read_auth(NfcWorker* nfc_worker) { furi_assert(nfc_worker); furi_assert(nfc_worker->callback); @@ -758,7 +900,13 @@ void nfc_worker_analyze_reader(NfcWorker* nfc_worker) { FuriHalNfcTxRxContext tx_rx = {}; ReaderAnalyzer* reader_analyzer = nfc_worker->reader_analyzer; - FuriHalNfcDevData* nfc_data = reader_analyzer_get_nfc_data(reader_analyzer); + FuriHalNfcDevData* nfc_data = NULL; + if(nfc_worker->dev_data->protocol == NfcDeviceProtocolMifareClassic) { + nfc_data = &nfc_worker->dev_data->nfc_data; + reader_analyzer_set_nfc_data(reader_analyzer, nfc_data); + } else { + nfc_data = reader_analyzer_get_nfc_data(reader_analyzer); + } MfClassicEmulator emulator = { .cuid = nfc_util_bytes2num(&nfc_data->uid[nfc_data->uid_len - 4], 4), .data = nfc_worker->dev_data->mf_classic_data, diff --git a/lib/nfc/nfc_worker.h b/lib/nfc/nfc_worker.h index 84615f5d8..ce3a18241 100644 --- a/lib/nfc/nfc_worker.h +++ b/lib/nfc/nfc_worker.h @@ -14,6 +14,8 @@ typedef enum { NfcWorkerStateUidEmulate, NfcWorkerStateMfUltralightEmulate, NfcWorkerStateMfClassicEmulate, + NfcWorkerStateMfClassicWrite, + NfcWorkerStateMfClassicUpdate, NfcWorkerStateReadMfUltralightReadAuth, NfcWorkerStateMfClassicDictAttack, NfcWorkerStateAnalyzeReader, @@ -48,13 +50,16 @@ typedef enum { NfcWorkerEventNoCardDetected, NfcWorkerEventWrongCardDetected, - // Mifare Classic events + // Read Mifare Classic events NfcWorkerEventNoDictFound, NfcWorkerEventNewSector, NfcWorkerEventNewDictKeyBatch, NfcWorkerEventFoundKeyA, NfcWorkerEventFoundKeyB, + // Write Mifare Classic events + NfcWorkerEventWrongCard, + // Detect Reader events NfcWorkerEventDetectReaderDetected, NfcWorkerEventDetectReaderLost, diff --git a/lib/nfc/nfc_worker_i.h b/lib/nfc/nfc_worker_i.h index 526182f9a..b9f69e620 100644 --- a/lib/nfc/nfc_worker_i.h +++ b/lib/nfc/nfc_worker_i.h @@ -41,6 +41,10 @@ void nfc_worker_emulate_mf_ultralight(NfcWorker* nfc_worker); void nfc_worker_emulate_mf_classic(NfcWorker* nfc_worker); +void nfc_worker_write_mf_classic(NfcWorker* nfc_worker); + +void nfc_worker_update_mf_classic(NfcWorker* nfc_worker); + void nfc_worker_mf_classic_dict_attack(NfcWorker* nfc_worker); void nfc_worker_mf_ultralight_read_auth(NfcWorker* nfc_worker); diff --git a/lib/nfc/protocols/crypto1.c b/lib/nfc/protocols/crypto1.c index f08164ba9..2ac0ff081 100644 --- a/lib/nfc/protocols/crypto1.c +++ b/lib/nfc/protocols/crypto1.c @@ -73,3 +73,55 @@ uint32_t prng_successor(uint32_t x, uint32_t n) { return SWAPENDIAN(x); } + +void crypto1_decrypt( + Crypto1* crypto, + uint8_t* encrypted_data, + uint16_t encrypted_data_bits, + uint8_t* decrypted_data) { + furi_assert(crypto); + furi_assert(encrypted_data); + furi_assert(decrypted_data); + + if(encrypted_data_bits < 8) { + uint8_t decrypted_byte = 0; + decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 0)) << 0; + decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 1)) << 1; + decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 2)) << 2; + decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 3)) << 3; + decrypted_data[0] = decrypted_byte; + } else { + for(size_t i = 0; i < encrypted_data_bits / 8; i++) { + decrypted_data[i] = crypto1_byte(crypto, 0, 0) ^ encrypted_data[i]; + } + } +} + +void crypto1_encrypt( + Crypto1* crypto, + uint8_t* keystream, + uint8_t* plain_data, + uint16_t plain_data_bits, + uint8_t* encrypted_data, + uint8_t* encrypted_parity) { + furi_assert(crypto); + furi_assert(plain_data); + furi_assert(encrypted_data); + furi_assert(encrypted_parity); + + if(plain_data_bits < 8) { + encrypted_data[0] = 0; + for(size_t i = 0; i < plain_data_bits; i++) { + encrypted_data[0] |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(plain_data[0], i)) << i; + } + } else { + memset(encrypted_parity, 0, plain_data_bits / 8 + 1); + for(uint8_t i = 0; i < plain_data_bits / 8; i++) { + encrypted_data[i] = crypto1_byte(crypto, keystream ? keystream[i] : 0, 0) ^ + plain_data[i]; + encrypted_parity[i / 8] |= + (((crypto1_filter(crypto->odd) ^ nfc_util_odd_parity8(plain_data[i])) & 0x01) + << (7 - (i & 0x0007))); + } + } +} diff --git a/lib/nfc/protocols/crypto1.h b/lib/nfc/protocols/crypto1.h index 07b39c22c..450d1534e 100644 --- a/lib/nfc/protocols/crypto1.h +++ b/lib/nfc/protocols/crypto1.h @@ -21,3 +21,17 @@ uint32_t crypto1_word(Crypto1* crypto1, uint32_t in, int is_encrypted); uint32_t crypto1_filter(uint32_t in); uint32_t prng_successor(uint32_t x, uint32_t n); + +void crypto1_decrypt( + Crypto1* crypto, + uint8_t* encrypted_data, + uint16_t encrypted_data_bits, + uint8_t* decrypted_data); + +void crypto1_encrypt( + Crypto1* crypto, + uint8_t* keystream, + uint8_t* plain_data, + uint16_t plain_data_bits, + uint8_t* encrypted_data, + uint8_t* encrypted_parity); diff --git a/lib/nfc/protocols/mifare_classic.c b/lib/nfc/protocols/mifare_classic.c index e879ff4ef..7b0e17975 100644 --- a/lib/nfc/protocols/mifare_classic.c +++ b/lib/nfc/protocols/mifare_classic.c @@ -9,21 +9,8 @@ #define MF_CLASSIC_AUTH_KEY_A_CMD (0x60U) #define MF_CLASSIC_AUTH_KEY_B_CMD (0x61U) -#define MF_CLASSIC_READ_SECT_CMD (0x30) - -typedef enum { - MfClassicActionDataRead, - MfClassicActionDataWrite, - MfClassicActionDataInc, - MfClassicActionDataDec, - - MfClassicActionKeyARead, - MfClassicActionKeyAWrite, - MfClassicActionKeyBRead, - MfClassicActionKeyBWrite, - MfClassicActionACRead, - MfClassicActionACWrite, -} MfClassicAction; +#define MF_CLASSIC_READ_BLOCK_CMD (0x30) +#define MF_CLASSIC_WRITE_BLOCK_CMD (0xA0) const char* mf_classic_get_type_str(MfClassicType type) { if(type == MfClassicType1k) { @@ -122,6 +109,24 @@ void mf_classic_set_block_read(MfClassicData* data, uint8_t block_num, MfClassic FURI_BIT_SET(data->block_read_mask[block_num / 32], block_num % 32); } +bool mf_classic_is_sector_data_read(MfClassicData* data, uint8_t sector_num) { + furi_assert(data); + + uint8_t first_block = mf_classic_get_first_block_num_of_sector(sector_num); + uint8_t total_blocks = mf_classic_get_blocks_num_in_sector(sector_num); + bool data_read = true; + for(size_t i = first_block; i < first_block + total_blocks; i++) { + data_read &= mf_classic_is_block_read(data, i); + } + + return data_read; +} + +void mf_classic_set_sector_data_not_read(MfClassicData* data) { + furi_assert(data); + memset(data->block_read_mask, 0, sizeof(data->block_read_mask)); +} + bool mf_classic_is_key_found(MfClassicData* data, uint8_t sector_num, MfClassicKey key_type) { furi_assert(data); @@ -190,6 +195,9 @@ void mf_classic_get_read_sectors_and_keys( uint8_t* sectors_read, uint8_t* keys_found) { furi_assert(data); + furi_assert(sectors_read); + furi_assert(keys_found); + *sectors_read = 0; *keys_found = 0; uint8_t sectors_total = mf_classic_get_total_sectors_num(data->type); @@ -225,12 +233,12 @@ bool mf_classic_is_card_read(MfClassicData* data) { return card_read; } -static bool mf_classic_is_allowed_access_sector_trailer( - MfClassicEmulator* emulator, +bool mf_classic_is_allowed_access_sector_trailer( + MfClassicData* data, uint8_t block_num, MfClassicKey key, MfClassicAction action) { - uint8_t* sector_trailer = emulator->data.block[block_num].value; + uint8_t* sector_trailer = data->block[block_num].value; uint8_t AC = ((sector_trailer[7] >> 5) & 0x04) | ((sector_trailer[8] >> 2) & 0x02) | ((sector_trailer[8] >> 7) & 0x01); switch(action) { @@ -266,13 +274,13 @@ static bool mf_classic_is_allowed_access_sector_trailer( return true; } -static bool mf_classic_is_allowed_access_data_block( - MfClassicEmulator* emulator, +bool mf_classic_is_allowed_access_data_block( + MfClassicData* data, uint8_t block_num, MfClassicKey key, MfClassicAction action) { uint8_t* sector_trailer = - emulator->data.block[mf_classic_get_sector_trailer_num_by_block(block_num)].value; + data->block[mf_classic_get_sector_trailer_num_by_block(block_num)].value; uint8_t sector_block; if(block_num <= 128) { @@ -336,9 +344,10 @@ static bool mf_classic_is_allowed_access( MfClassicKey key, MfClassicAction action) { if(mf_classic_is_sector_trailer(block_num)) { - return mf_classic_is_allowed_access_sector_trailer(emulator, block_num, key, action); + return mf_classic_is_allowed_access_sector_trailer( + &emulator->data, block_num, key, action); } else { - return mf_classic_is_allowed_access_data_block(emulator, block_num, key, action); + return mf_classic_is_allowed_access_data_block(&emulator->data, block_num, key, action); } } @@ -514,25 +523,17 @@ bool mf_classic_read_block( furi_assert(block); bool read_block_success = false; - uint8_t plain_cmd[4] = {MF_CLASSIC_READ_SECT_CMD, block_num, 0x00, 0x00}; + uint8_t plain_cmd[4] = {MF_CLASSIC_READ_BLOCK_CMD, block_num, 0x00, 0x00}; nfca_append_crc16(plain_cmd, 2); - memset(tx_rx->tx_data, 0, sizeof(tx_rx->tx_data)); - memset(tx_rx->tx_parity, 0, sizeof(tx_rx->tx_parity)); - for(uint8_t i = 0; i < 4; i++) { - tx_rx->tx_data[i] = crypto1_byte(crypto, 0x00, 0) ^ plain_cmd[i]; - tx_rx->tx_parity[0] |= - ((crypto1_filter(crypto->odd) ^ nfc_util_odd_parity8(plain_cmd[i])) & 0x01) << (7 - i); - } + crypto1_encrypt(crypto, NULL, plain_cmd, 4 * 8, tx_rx->tx_data, tx_rx->tx_parity); tx_rx->tx_bits = 4 * 9; tx_rx->tx_rx_type = FuriHalNfcTxRxTypeRaw; if(furi_hal_nfc_tx_rx(tx_rx, 50)) { if(tx_rx->rx_bits == 8 * (MF_CLASSIC_BLOCK_SIZE + 2)) { uint8_t block_received[MF_CLASSIC_BLOCK_SIZE + 2]; - for(uint8_t i = 0; i < MF_CLASSIC_BLOCK_SIZE + 2; i++) { - block_received[i] = crypto1_byte(crypto, 0, 0) ^ tx_rx->rx_data[i]; - } + crypto1_decrypt(crypto, tx_rx->rx_data, tx_rx->rx_bits, block_received); uint16_t crc_calc = nfca_get_crc16(block_received, MF_CLASSIC_BLOCK_SIZE); uint16_t crc_received = (block_received[MF_CLASSIC_BLOCK_SIZE + 1] << 8) | block_received[MF_CLASSIC_BLOCK_SIZE]; @@ -754,49 +755,6 @@ uint8_t mf_classic_update_card(FuriHalNfcTxRxContext* tx_rx, MfClassicData* data return sectors_read; } -void mf_crypto1_decrypt( - Crypto1* crypto, - uint8_t* encrypted_data, - uint16_t encrypted_data_bits, - uint8_t* decrypted_data) { - if(encrypted_data_bits < 8) { - uint8_t decrypted_byte = 0; - decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 0)) << 0; - decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 1)) << 1; - decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 2)) << 2; - decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 3)) << 3; - decrypted_data[0] = decrypted_byte; - } else { - for(size_t i = 0; i < encrypted_data_bits / 8; i++) { - decrypted_data[i] = crypto1_byte(crypto, 0, 0) ^ encrypted_data[i]; - } - } -} - -void mf_crypto1_encrypt( - Crypto1* crypto, - uint8_t* keystream, - uint8_t* plain_data, - uint16_t plain_data_bits, - uint8_t* encrypted_data, - uint8_t* encrypted_parity) { - if(plain_data_bits < 8) { - encrypted_data[0] = 0; - for(size_t i = 0; i < plain_data_bits; i++) { - encrypted_data[0] |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(plain_data[0], i)) << i; - } - } else { - memset(encrypted_parity, 0, plain_data_bits / 8 + 1); - for(uint8_t i = 0; i < plain_data_bits / 8; i++) { - encrypted_data[i] = crypto1_byte(crypto, keystream ? keystream[i] : 0, 0) ^ - plain_data[i]; - encrypted_parity[i / 8] |= - (((crypto1_filter(crypto->odd) ^ nfc_util_odd_parity8(plain_data[i])) & 0x01) - << (7 - (i & 0x0007))); - } - } -} - bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_rx) { furi_assert(emulator); furi_assert(tx_rx); @@ -819,7 +777,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ tx_rx->rx_bits); break; } - mf_crypto1_decrypt(&emulator->crypto, tx_rx->rx_data, tx_rx->rx_bits, plain_data); + crypto1_decrypt(&emulator->crypto, tx_rx->rx_data, tx_rx->rx_bits, plain_data); } if(plain_data[0] == 0x50 && plain_data[1] == 0x00) { @@ -857,7 +815,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ tx_rx->tx_bits = sizeof(nt) * 8; tx_rx->tx_rx_type = FuriHalNfcTxRxTransparent; } else { - mf_crypto1_encrypt( + crypto1_encrypt( &emulator->crypto, nt_keystream, nt, @@ -904,7 +862,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ uint32_t ans = prng_successor(nonce, 96); uint8_t responce[4] = {}; nfc_util_num2bytes(ans, 4, responce); - mf_crypto1_encrypt( + crypto1_encrypt( &emulator->crypto, NULL, responce, @@ -938,7 +896,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ // Send NACK uint8_t nack = 0x04; if(is_encrypted) { - mf_crypto1_encrypt( + crypto1_encrypt( &emulator->crypto, NULL, &nack, 4, tx_rx->tx_data, tx_rx->tx_parity); } else { tx_rx->tx_data[0] = nack; @@ -951,7 +909,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ } nfca_append_crc16(block_data, 16); - mf_crypto1_encrypt( + crypto1_encrypt( &emulator->crypto, NULL, block_data, @@ -967,14 +925,14 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ } // Send ACK uint8_t ack = 0x0A; - mf_crypto1_encrypt(&emulator->crypto, NULL, &ack, 4, tx_rx->tx_data, tx_rx->tx_parity); + crypto1_encrypt(&emulator->crypto, NULL, &ack, 4, tx_rx->tx_data, tx_rx->tx_parity); tx_rx->tx_rx_type = FuriHalNfcTxRxTransparent; tx_rx->tx_bits = 4; if(!furi_hal_nfc_tx_rx(tx_rx, 300)) break; if(tx_rx->rx_bits != 18 * 8) break; - mf_crypto1_decrypt(&emulator->crypto, tx_rx->rx_data, tx_rx->rx_bits, plain_data); + crypto1_decrypt(&emulator->crypto, tx_rx->rx_data, tx_rx->rx_bits, plain_data); uint8_t block_data[16] = {}; memcpy(block_data, emulator->data.block[block].value, MF_CLASSIC_BLOCK_SIZE); if(mf_classic_is_sector_trailer(block)) { @@ -1002,7 +960,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ } // Send ACK ack = 0x0A; - mf_crypto1_encrypt(&emulator->crypto, NULL, &ack, 4, tx_rx->tx_data, tx_rx->tx_parity); + crypto1_encrypt(&emulator->crypto, NULL, &ack, 4, tx_rx->tx_data, tx_rx->tx_parity); tx_rx->tx_rx_type = FuriHalNfcTxRxTransparent; tx_rx->tx_bits = 4; } else { @@ -1015,8 +973,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ // Send NACK uint8_t nack = 0x04; if(is_encrypted) { - mf_crypto1_encrypt( - &emulator->crypto, NULL, &nack, 4, tx_rx->tx_data, tx_rx->tx_parity); + crypto1_encrypt(&emulator->crypto, NULL, &nack, 4, tx_rx->tx_data, tx_rx->tx_parity); } else { tx_rx->tx_data[0] = nack; } @@ -1027,3 +984,143 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ return true; } + +bool mf_classic_write_block( + FuriHalNfcTxRxContext* tx_rx, + MfClassicBlock* src_block, + uint8_t block_num, + MfClassicKey key_type, + uint64_t key) { + furi_assert(tx_rx); + furi_assert(src_block); + + Crypto1 crypto = {}; + uint8_t plain_data[18] = {}; + uint8_t resp = 0; + bool write_success = false; + + do { + furi_hal_nfc_sleep(); + if(!mf_classic_auth(tx_rx, block_num, key, key_type, &crypto)) { + FURI_LOG_D(TAG, "Auth fail"); + break; + } + // Send write command + plain_data[0] = MF_CLASSIC_WRITE_BLOCK_CMD; + plain_data[1] = block_num; + nfca_append_crc16(plain_data, 2); + crypto1_encrypt(&crypto, NULL, plain_data, 4 * 8, tx_rx->tx_data, tx_rx->tx_parity); + tx_rx->tx_bits = 4 * 8; + tx_rx->tx_rx_type = FuriHalNfcTxRxTypeRaw; + + if(furi_hal_nfc_tx_rx(tx_rx, 50)) { + if(tx_rx->rx_bits == 4) { + crypto1_decrypt(&crypto, tx_rx->rx_data, 4, &resp); + if(resp != 0x0A) { + FURI_LOG_D(TAG, "NACK received on write cmd: %02X", resp); + break; + } + } else { + FURI_LOG_D(TAG, "Not ACK received"); + break; + } + } else { + FURI_LOG_D(TAG, "Failed to send write cmd"); + break; + } + + // Send data + memcpy(plain_data, src_block->value, MF_CLASSIC_BLOCK_SIZE); + nfca_append_crc16(plain_data, MF_CLASSIC_BLOCK_SIZE); + crypto1_encrypt( + &crypto, + NULL, + plain_data, + (MF_CLASSIC_BLOCK_SIZE + 2) * 8, + tx_rx->tx_data, + tx_rx->tx_parity); + tx_rx->tx_bits = (MF_CLASSIC_BLOCK_SIZE + 2) * 8; + tx_rx->tx_rx_type = FuriHalNfcTxRxTypeRaw; + if(furi_hal_nfc_tx_rx(tx_rx, 50)) { + if(tx_rx->rx_bits == 4) { + crypto1_decrypt(&crypto, tx_rx->rx_data, 4, &resp); + if(resp != 0x0A) { + FURI_LOG_D(TAG, "NACK received on sending data"); + break; + } + } else { + FURI_LOG_D(TAG, "Not ACK received"); + break; + } + } else { + FURI_LOG_D(TAG, "Failed to send data"); + break; + } + write_success = true; + + // Send Halt + plain_data[0] = 0x50; + plain_data[1] = 0x00; + nfca_append_crc16(plain_data, 2); + crypto1_encrypt(&crypto, NULL, plain_data, 2 * 8, tx_rx->tx_data, tx_rx->tx_parity); + tx_rx->tx_bits = 2 * 8; + tx_rx->tx_rx_type = FuriHalNfcTxRxTypeRaw; + // No response is expected + furi_hal_nfc_tx_rx(tx_rx, 50); + } while(false); + + return write_success; +} + +bool mf_classic_write_sector( + FuriHalNfcTxRxContext* tx_rx, + MfClassicData* dest_data, + MfClassicData* src_data, + uint8_t sec_num) { + furi_assert(tx_rx); + furi_assert(dest_data); + furi_assert(src_data); + + uint8_t first_block = mf_classic_get_first_block_num_of_sector(sec_num); + uint8_t total_blocks = mf_classic_get_blocks_num_in_sector(sec_num); + MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(dest_data, sec_num); + bool key_a_found = mf_classic_is_key_found(dest_data, sec_num, MfClassicKeyA); + bool key_b_found = mf_classic_is_key_found(dest_data, sec_num, MfClassicKeyB); + + bool write_success = true; + for(size_t i = first_block; i < first_block + total_blocks; i++) { + // Compare blocks + if(memcmp(dest_data->block[i].value, src_data->block[i].value, MF_CLASSIC_BLOCK_SIZE)) { + bool key_a_write_allowed = mf_classic_is_allowed_access_data_block( + dest_data, i, MfClassicKeyA, MfClassicActionDataWrite); + bool key_b_write_allowed = mf_classic_is_allowed_access_data_block( + dest_data, i, MfClassicKeyB, MfClassicActionDataWrite); + + if(key_a_found && key_a_write_allowed) { + FURI_LOG_I(TAG, "Writing block %d with key A", i); + uint64_t key = nfc_util_bytes2num(sec_tr->key_a, 6); + if(!mf_classic_write_block(tx_rx, &src_data->block[i], i, MfClassicKeyA, key)) { + FURI_LOG_E(TAG, "Failed to write block %d", i); + write_success = false; + break; + } + } else if(key_b_found && key_b_write_allowed) { + FURI_LOG_I(TAG, "Writing block %d with key A", i); + uint64_t key = nfc_util_bytes2num(sec_tr->key_b, 6); + if(!mf_classic_write_block(tx_rx, &src_data->block[i], i, MfClassicKeyB, key)) { + FURI_LOG_E(TAG, "Failed to write block %d", i); + write_success = false; + break; + } + } else { + FURI_LOG_E(TAG, "Failed to find key with write access"); + write_success = false; + break; + } + } else { + FURI_LOG_D(TAG, "Blocks %d are equal", i); + } + } + + return write_success; +} diff --git a/lib/nfc/protocols/mifare_classic.h b/lib/nfc/protocols/mifare_classic.h index ead846e42..d5467b100 100644 --- a/lib/nfc/protocols/mifare_classic.h +++ b/lib/nfc/protocols/mifare_classic.h @@ -27,6 +27,20 @@ typedef enum { MfClassicKeyB, } MfClassicKey; +typedef enum { + MfClassicActionDataRead, + MfClassicActionDataWrite, + MfClassicActionDataInc, + MfClassicActionDataDec, + + MfClassicActionKeyARead, + MfClassicActionKeyAWrite, + MfClassicActionKeyBRead, + MfClassicActionKeyBWrite, + MfClassicActionACRead, + MfClassicActionACWrite, +} MfClassicAction; + typedef struct { uint8_t value[MF_CLASSIC_BLOCK_SIZE]; } MfClassicBlock; @@ -90,6 +104,18 @@ bool mf_classic_is_sector_trailer(uint8_t block); uint8_t mf_classic_get_sector_by_block(uint8_t block); +bool mf_classic_is_allowed_access_sector_trailer( + MfClassicData* data, + uint8_t block_num, + MfClassicKey key, + MfClassicAction action); + +bool mf_classic_is_allowed_access_data_block( + MfClassicData* data, + uint8_t block_num, + MfClassicKey key, + MfClassicAction action); + bool mf_classic_is_key_found(MfClassicData* data, uint8_t sector_num, MfClassicKey key_type); void mf_classic_set_key_found( @@ -104,6 +130,10 @@ bool mf_classic_is_block_read(MfClassicData* data, uint8_t block_num); void mf_classic_set_block_read(MfClassicData* data, uint8_t block_num, MfClassicBlock* block_data); +bool mf_classic_is_sector_data_read(MfClassicData* data, uint8_t sector_num); + +void mf_classic_set_sector_data_not_read(MfClassicData* data); + bool mf_classic_is_sector_read(MfClassicData* data, uint8_t sector_num); bool mf_classic_is_card_read(MfClassicData* data); @@ -145,3 +175,16 @@ uint8_t mf_classic_read_card( uint8_t mf_classic_update_card(FuriHalNfcTxRxContext* tx_rx, MfClassicData* data); bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_rx); + +bool mf_classic_write_block( + FuriHalNfcTxRxContext* tx_rx, + MfClassicBlock* src_block, + uint8_t block_num, + MfClassicKey key_type, + uint64_t key); + +bool mf_classic_write_sector( + FuriHalNfcTxRxContext* tx_rx, + MfClassicData* dest_data, + MfClassicData* src_data, + uint8_t sec_num); From 5277933980dc8933d387620b4c3da42f02dee2dd Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:12:31 +0300 Subject: [PATCH 09/14] finish icon changes --- .../plugins/flipper_i2ctools/application.fam | 1 + .../flipper_i2ctools/images/ButtonDown_7x4.png | Bin 0 -> 102 bytes .../flipper_i2ctools/images/ButtonLeft_4x7.png | Bin 0 -> 1415 bytes .../flipper_i2ctools/images/ButtonRight_4x7.png | Bin 0 -> 1839 bytes .../flipper_i2ctools/images/ButtonUp_7x4.png | Bin 0 -> 102 bytes .../flipper_i2ctools/images/Ok_btn_9x9.png | Bin 0 -> 3605 bytes .../images/passport_bad3_46x49.png | Bin 0 -> 1304 bytes .../images/passport_happy2_46x49.png | Bin 0 -> 1328 bytes .../images/passport_happy3_46x49.png | Bin 0 -> 1348 bytes .../plugins/flipper_i2ctools/views/main_view.h | 1 + .../flipper_i2ctools/views/scanner_view.h | 2 ++ .../plugins/flipper_i2ctools/views/sender_view.h | 2 ++ .../flipper_i2ctools/views/sniffer_view.h | 2 ++ applications/plugins/playlist/application.fam | 1 + .../plugins/playlist/images/ButtonRight_4x7.png | Bin 0 -> 1839 bytes .../plugins/playlist/images/sub1_10px.png | Bin 0 -> 299 bytes applications/plugins/playlist/playlist.c | 2 +- 17 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 applications/plugins/flipper_i2ctools/images/ButtonDown_7x4.png create mode 100644 applications/plugins/flipper_i2ctools/images/ButtonLeft_4x7.png create mode 100644 applications/plugins/flipper_i2ctools/images/ButtonRight_4x7.png create mode 100644 applications/plugins/flipper_i2ctools/images/ButtonUp_7x4.png create mode 100644 applications/plugins/flipper_i2ctools/images/Ok_btn_9x9.png create mode 100644 applications/plugins/flipper_i2ctools/images/passport_bad3_46x49.png create mode 100644 applications/plugins/flipper_i2ctools/images/passport_happy2_46x49.png create mode 100644 applications/plugins/flipper_i2ctools/images/passport_happy3_46x49.png create mode 100644 applications/plugins/playlist/images/ButtonRight_4x7.png create mode 100644 applications/plugins/playlist/images/sub1_10px.png diff --git a/applications/plugins/flipper_i2ctools/application.fam b/applications/plugins/flipper_i2ctools/application.fam index 470947b1e..6fafeef58 100644 --- a/applications/plugins/flipper_i2ctools/application.fam +++ b/applications/plugins/flipper_i2ctools/application.fam @@ -9,4 +9,5 @@ App( order=175, fap_icon="i2ctools.png", fap_category="GPIO", + fap_icon_assets="images", ) \ No newline at end of file diff --git a/applications/plugins/flipper_i2ctools/images/ButtonDown_7x4.png b/applications/plugins/flipper_i2ctools/images/ButtonDown_7x4.png new file mode 100644 index 0000000000000000000000000000000000000000..2954bb6a67d1c23c0bb5d765e8d2aa04b9b5adec GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)I!3HFqj;YoHDIHH2#}J9|(o>FH3<^BV2haYO z-y5_sM4;GPjq%Ck6>60csmUj6EiNa>ORduPH4*)h!w|e3sE@(Z)z4*}Q$iC10Gods AV*mgE literal 0 HcmV?d00001 diff --git a/applications/plugins/flipper_i2ctools/images/ButtonLeft_4x7.png b/applications/plugins/flipper_i2ctools/images/ButtonLeft_4x7.png new file mode 100644 index 0000000000000000000000000000000000000000..0b4655d43247083aa705620e9836ac415b42ca46 GIT binary patch literal 1415 zcmbVM+iKK67*5rq)>aU2M7$VM1Vxif;vTv~W2u`S7ED{V3s&&L*<`XiG|9wd+THd> z5CnY!sdyuJtrvQyAo>KpiLcV|{Tkc)riAbluXfwSZCApL`ztB&p zx6LGKvks4K_4~)qD&oGa-YdJlW)hAKMNJd7<=t?6c^RI1>c$ifyjaM>^|&8!ey zB4!nh9u>5uen6Ve@<H5rru6h<2Ef#GQdQ*CmZOlQi~N!?9H`Rp;C% zU}CB21#?;r`&0|6C0}b-=jODa5|nEJ#ntxQ&{~jpgtwDta4hftr~G=#p@V36e4Zjh zq%J~{y26Jjn=1Nw-l*3%QW5YFE*v4z3gt0$&(*xf2en34c?JpH8+FYldo+Alvg8af-pG4(=!fyUi-Wsg z`g#n9VUcf(DFr{poMSNzw-lz>w+HV+n1ELr&SLA#LHUb0p(xWQ(1*vJ-i+1!`swxZ Z!O7;c$;lT_->m1Ovaz)0yuI`A$q$F8u*d)a literal 0 HcmV?d00001 diff --git a/applications/plugins/flipper_i2ctools/images/ButtonRight_4x7.png b/applications/plugins/flipper_i2ctools/images/ButtonRight_4x7.png new file mode 100644 index 0000000000000000000000000000000000000000..8e1c74c1c0038ea55172f19ac875003fc80c2d06 GIT binary patch literal 1839 zcmcIlO>f*p7#Yw)M6zw!O+@VZ{?d|D~WYi~8rHRY?X-&T}Yen`g$^+EJ;z+|RV zE@PoDvZ9%#+_}3bC_5Cj8jDGq541mi{7F+&KF}W65sr$Xn5H|YrMQ2(J7%Yc%;(zO z57ax000=TsQ+1Ke@+w#iw3au3cGGQWY740k2ijH>P(6tD)S)be>gX6Tj7`<`b>di- zgWp$8Y+?i31~CzF0&E4uRlA=C(Mp~K`{74jEchB|)4DDK!ZVhSwdFyw0YIZ1cDh0S{OvfO-U_~ zvmRF*m9sWDXNH)GOyqS1Skhxbr6}s*7t&@~kFM(NW5}qh?Lu@lJ}HE;FDiLdGO>LO z5pS*%E2grR)l^;|?O5b_?u0me&c1U}%jrk8*%=Wk%i)8yp2P|kuxmKg<=(u_`oQRI_0 zS`-DNysBx=#3&qSkgA@hJP>~D+ZM(s5jI6Owp`?yE=3e`YGUqkVOp#Cp=3wR3O4hX zX6BLsN3UBzV(vI5;|SZHgOb=HD0VFjpTyfFW}GnQuh>2*Q`k>*cAmA#iUT7EXSpo# zkPm5~#I-o^cpgfe#P$=4-Pi*SpT!-@nJgp8L347xe>5EKl`=_ZFc8XGy+_j=_R_7! z@vZZMowS1GJ?Zw)eetks%~G{BTR>T}9|jt0j3Btyb*C3-`C?fwY3EY`q*oYZ39DpM z&uJ;PCZPLs4QO1Jd_|A1PF)azZJ)RZ`^-VMWr6e#XUOA%3eLG_Ch@BDOHzMk*MF0G zCo7xMd?Mg*HMIXw%nNz?%60fZiZPlqb?GqUpXO`F&Yi!okZl(n>P@r1P2i)yk3DgRwbHeNn6e|;J^SK4TM LH~i+q&mR8;k>NTA literal 0 HcmV?d00001 diff --git a/applications/plugins/flipper_i2ctools/images/ButtonUp_7x4.png b/applications/plugins/flipper_i2ctools/images/ButtonUp_7x4.png new file mode 100644 index 0000000000000000000000000000000000000000..1be79328b40a93297a5609756328406565c437c0 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)I!3HFqj;YoHDIHH2#}J8d-yTOk1_O>mFaFD) zeWb+ZHz{mGZZ1QpXe09^4tcYT#4oe=UbmGC^A-KE*|F&zP#=S*tDnm{r-UX30HgpM AM*si- literal 0 HcmV?d00001 diff --git a/applications/plugins/flipper_i2ctools/images/Ok_btn_9x9.png b/applications/plugins/flipper_i2ctools/images/Ok_btn_9x9.png new file mode 100644 index 0000000000000000000000000000000000000000..9a1539da2049f12f7b25f96b11a9c40cd8227302 GIT binary patch literal 3605 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 literal 0 HcmV?d00001 diff --git a/applications/plugins/flipper_i2ctools/images/passport_happy2_46x49.png b/applications/plugins/flipper_i2ctools/images/passport_happy2_46x49.png new file mode 100644 index 0000000000000000000000000000000000000000..f64e770e5a038162d1ceae38d04ff043319ed581 GIT binary patch literal 1328 zcmaJ>Yitx%6rNHagjluO2sA~8L5mM&=dm-pJCklnyW7Xw?z-8ow1|+-+_|%zwDWLg z*4=JQ4T2E_{h_2)G0`ZMkTi{u5Qw0Lw#J|dQHh8c!L(6Iq@h|9+oV|Uw%y7P!Xz{I z-gD1)&Uaq3Cmw4kSy8?M!?2P_V>p3U4|IFeIR98utqxMUo{T%Nuc)W*Lg+D25|xTJ#Dc$Ki_)f!x`O zDkj49i_Xv~NOZWaB~nx-lksG{9@9=yj35XU%~C8&A`Q~%He4x78qWHHg)nr0ty!*S z8hGBKB%5hBNFb^UG3Zt_x@6dJ7Bhu%Mr9?7VmvgZ>-oUuwH-GB|EFWHw{0D zm3rIM@%c21+AS#f6e!CaDz?C?EXi>^AO%6;Nx$NQDjnchuZqH7z$-VUZ=p|-1chN0 z*oc7ftGo~RNQr?e$q1a649EbIlAq=SD(_b~FHKO0B9-r)n>wi=LhYH~E)51cER7a4 z&^*~_aK^uDI){etu6T2@Zfd*YjtfxXp%eGRQg&lXYD z>C^k^p0%q=)2sVF6f4_;?|fQ5nD}e+WMi(s`rs$KPR)#LId=cV_R~+^*cMxxJJdgV zw0&8_+g)6A`PQ$~Hy$Y)&plLe;h6CKop*03e)#I^J9|&;h*gA2hyRHmn118qjZ-DT zYs5z{4S%Iqzp~Og+&k7g_(c1f%Bs?t%uxEZ{r1o=vmcyD_N6DLe{8kRrvkelywvq; z`Q^g{dk0@NX5Y;``_suk`^t}iYXe6{)aRPFZu@fLk9C(o@0F3?Ce_;EL2>Mdvm3V0 z_HUjky{>&ex-K?+CR4f6soHSh^7(%sEv3pv->1q>-LVRrpo*u*!||g0KP*xg3lG+| Gzx*E~RoOxS literal 0 HcmV?d00001 diff --git a/applications/plugins/flipper_i2ctools/images/passport_happy3_46x49.png b/applications/plugins/flipper_i2ctools/images/passport_happy3_46x49.png new file mode 100644 index 0000000000000000000000000000000000000000..7aef17674336e71cb1cdf323c1fb039e0e627d33 GIT binary patch literal 1348 zcmaJ>eQXnD7{7&~lejEf6dZ`TCJQE*5i}V=Gtg{i3Pxj)$VR4uj%5Br(2>QcxJ9tO-PXw;3zyuz@B6&Z z?|FXD*S*$I_h|8o)hiGLDGt_15x6?wOBF4H-?y&BYT;6D`y1`erf#QY3m{(2Q~(-` z%S|8xWUYP2H^7Y`%eswdqum$|iK-cQ$T=NHCZ2?71aWW5BxN-QY*YbFM#6(l4~<}` zp?R>UxG)(``arW$(_w+l9d%K)Bc=)(wrL~k&WO-J9N03NiMJ$DV#b5b*%jeFCnhj- zPQ{LSuz6CA;Re)aS^(u86t0paiSmL&lNDK2lnp3N(iB0m1jXVcDKdh{vgpEtL3fs> zixDZX;0&HTShH;>MS@7D(~dObFs&wn5(I%DX@aJ4sDY>26Skbe6RGui3ld1FmXWj# zGlAwT%8J=)doW0KK8AQQ99}e>NG)Uv=8VY5NrG~aL_D4gY)(66N5KCymefu~+mnEZ zfRx#4sjwjW`aBpW@Ai&zija+1ZyB&Ea*JfDt#OdBgOUe>HxA9vM4bc*$0-`F0Gh{H zMo@8?BRQPYR8HkN!AUA=-p*2ZUSj9~!%3{G+DlP>pNr)J66584924*d=;}N+m`K@j zLIru>2K2pv_1zXL`Ya&ZrWG~KmV6sDG@G`WYBrN7%{WN(p|GqPiJYV|SEc!&C14qC zKnqxA9Gy$EXe>d&sR2b{VX*~Tr*W3$R9p}=4(Bx|&`B3dGdc`^9{kf_iiuqr*cMf-r|D(OMvukwvy8`o1`tw&u9$(pU5#95~ z%oTI>8uxp$)MLN!$|u8Ts>&uiuO7ZWBpiL98lP5}6NArIJQJ=spqB+Br+<8N>Vw_S z_Y<{yCf_|@e7Vx~+0Lm4rC5)>;nUic6RxqR?aGg??}}}We<5Oh`o+G7&-IS&?LRe} z-r_uRFyy2dJyS4Wz?o~hpirfB!b{SC`cogKU3Sa;{l$NW9Rh4&#f XzZTxR@26EIx&K #include #include +#include #define APP_NAME "I2C Tools" diff --git a/applications/plugins/flipper_i2ctools/views/scanner_view.h b/applications/plugins/flipper_i2ctools/views/scanner_view.h index 249a12942..99ba3c29f 100644 --- a/applications/plugins/flipper_i2ctools/views/scanner_view.h +++ b/applications/plugins/flipper_i2ctools/views/scanner_view.h @@ -2,6 +2,8 @@ #include #include +#include + #include "../i2cscanner.h" #define SCAN_MENU_TEXT "Scan" diff --git a/applications/plugins/flipper_i2ctools/views/sender_view.h b/applications/plugins/flipper_i2ctools/views/sender_view.h index d0ab28ba4..866e483aa 100644 --- a/applications/plugins/flipper_i2ctools/views/sender_view.h +++ b/applications/plugins/flipper_i2ctools/views/sender_view.h @@ -2,6 +2,8 @@ #include #include +#include + #include "../i2csender.h" #define SEND_MENU_TEXT "Send" diff --git a/applications/plugins/flipper_i2ctools/views/sniffer_view.h b/applications/plugins/flipper_i2ctools/views/sniffer_view.h index da46aaef1..56a213895 100644 --- a/applications/plugins/flipper_i2ctools/views/sniffer_view.h +++ b/applications/plugins/flipper_i2ctools/views/sniffer_view.h @@ -2,6 +2,8 @@ #include #include +#include + #include "../i2csniffer.h" #define SNIFF_MENU_TEXT "Sniff" diff --git a/applications/plugins/playlist/application.fam b/applications/plugins/playlist/application.fam index 8be27aa6c..fda109dab 100644 --- a/applications/plugins/playlist/application.fam +++ b/applications/plugins/playlist/application.fam @@ -9,4 +9,5 @@ App( order=14, fap_icon="playlist_10px.png", fap_category="Tools", + fap_icon_assets="images", ) diff --git a/applications/plugins/playlist/images/ButtonRight_4x7.png b/applications/plugins/playlist/images/ButtonRight_4x7.png new file mode 100644 index 0000000000000000000000000000000000000000..8e1c74c1c0038ea55172f19ac875003fc80c2d06 GIT binary patch literal 1839 zcmcIlO>f*p7#Yw)M6zw!O+@VZ{?d|D~WYi~8rHRY?X-&T}Yen`g$^+EJ;z+|RV zE@PoDvZ9%#+_}3bC_5Cj8jDGq541mi{7F+&KF}W65sr$Xn5H|YrMQ2(J7%Yc%;(zO z57ax000=TsQ+1Ke@+w#iw3au3cGGQWY740k2ijH>P(6tD)S)be>gX6Tj7`<`b>di- zgWp$8Y+?i31~CzF0&E4uRlA=C(Mp~K`{74jEchB|)4DDK!ZVhSwdFyw0YIZ1cDh0S{OvfO-U_~ zvmRF*m9sWDXNH)GOyqS1Skhxbr6}s*7t&@~kFM(NW5}qh?Lu@lJ}HE;FDiLdGO>LO z5pS*%E2grR)l^;|?O5b_?u0me&c1U}%jrk8*%=Wk%i)8yp2P|kuxmKg<=(u_`oQRI_0 zS`-DNysBx=#3&qSkgA@hJP>~D+ZM(s5jI6Owp`?yE=3e`YGUqkVOp#Cp=3wR3O4hX zX6BLsN3UBzV(vI5;|SZHgOb=HD0VFjpTyfFW}GnQuh>2*Q`k>*cAmA#iUT7EXSpo# zkPm5~#I-o^cpgfe#P$=4-Pi*SpT!-@nJgp8L347xe>5EKl`=_ZFc8XGy+_j=_R_7! z@vZZMowS1GJ?Zw)eetks%~G{BTR>T}9|jt0j3Btyb*C3-`C?fwY3EY`q*oYZ39DpM z&uJ;PCZPLs4QO1Jd_|A1PF)azZJ)RZ`^-VMWr6e#XUOA%3eLG_Ch@BDOHzMk*MF0G zCo7xMd?Mg*HMIXw%nNz?%60fZiZPlqb?GqUpXO`F&Yi!okZl(n>P@r1P2i)yk3DgRwbHeNn6e|;J^SK4TM LH~i+q&mR8;k>NTA literal 0 HcmV?d00001 diff --git a/applications/plugins/playlist/images/sub1_10px.png b/applications/plugins/playlist/images/sub1_10px.png new file mode 100644 index 0000000000000000000000000000000000000000..5a25fdf4ef1c6cf53634aa74675001a3e8c85b7b GIT binary patch literal 299 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xkYHHq`AGmsv7|ftIx;Y9?C1WI$O_~uBzpw; zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpafHrx4R1i<>&pI=m5)cB{fFDGZlI8yr;B3<$MxhJ?+;A4eL&#) z0Ra}bue?07WhLz78x$BO|L3mq-MMxdP^D^#YeY#(Vo9o1a#1RfVlXl=GSoFN)ipE; zF*LF=Hn1|b&^9ozGB8+QQTzo(LvDUbW?CgwgE3G~h=Hk #include -#include +#include #include #include From 3ea6d59c2fa71176a70a4015dc460bf1157a41f8 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:18:42 +0300 Subject: [PATCH 10/14] fmt + update subbrute submodule --- applications/plugins/snake_game/snake_game.c | 3 +- applications/plugins/subbrute | 2 +- .../scenes/app_settings/totp_app_settings.c | 4 +- .../scenes/app_settings/totp_app_settings.h | 4 +- .../authenticate/totp_scene_authenticate.c | 4 +- .../authenticate/totp_scene_authenticate.h | 4 +- .../plugins/totp/services/ui/ui_controls.c | 6 ++- .../plugins/totp/services/ui/ui_controls.h | 6 ++- applications/plugins/totp/totp_app.c | 44 +++++++++++++------ 9 files changed, 54 insertions(+), 23 deletions(-) diff --git a/applications/plugins/snake_game/snake_game.c b/applications/plugins/snake_game/snake_game.c index 6429d8f25..2867134bc 100644 --- a/applications/plugins/snake_game/snake_game.c +++ b/applications/plugins/snake_game/snake_game.c @@ -41,7 +41,7 @@ typedef enum { DirectionLeft, } Direction; -#define MAX_SNAKE_LEN 128*64/4 +#define MAX_SNAKE_LEN 128 * 64 / 4 typedef struct { Point points[MAX_SNAKE_LEN]; @@ -268,7 +268,6 @@ static void return; } - snake_state->currentMovement = snake_game_get_turn_snake(snake_state); Point next_step = snake_game_get_next_step(snake_state); diff --git a/applications/plugins/subbrute b/applications/plugins/subbrute index 936023c8f..ed49dcbfc 160000 --- a/applications/plugins/subbrute +++ b/applications/plugins/subbrute @@ -1 +1 @@ -Subproject commit 936023c8fba8fd015bcf97c36a63b13c624cef0c +Subproject commit ed49dcbfcd2e86418c8a8603e7ae9db3a5de185d diff --git a/applications/plugins/totp/scenes/app_settings/totp_app_settings.c b/applications/plugins/totp/scenes/app_settings/totp_app_settings.c index 4e03de433..ea9a11e9b 100644 --- a/applications/plugins/totp/scenes/app_settings/totp_app_settings.c +++ b/applications/plugins/totp/scenes/app_settings/totp_app_settings.c @@ -90,7 +90,9 @@ void totp_scene_app_settings_render(Canvas* const canvas, PluginState* plugin_st scene_state->selected_control == ConfirmButton); } -bool totp_scene_app_settings_handle_event(const PluginEvent* const event, PluginState* plugin_state) { +bool totp_scene_app_settings_handle_event( + const PluginEvent* const event, + PluginState* plugin_state) { if(event->type == EventTypeKey) { SceneState* scene_state = (SceneState*)plugin_state->current_scene_state; if(event->input.type == InputTypePress) { diff --git a/applications/plugins/totp/scenes/app_settings/totp_app_settings.h b/applications/plugins/totp/scenes/app_settings/totp_app_settings.h index be51e9dc9..e965d79b7 100644 --- a/applications/plugins/totp/scenes/app_settings/totp_app_settings.h +++ b/applications/plugins/totp/scenes/app_settings/totp_app_settings.h @@ -15,6 +15,8 @@ void totp_scene_app_settings_activate( PluginState* plugin_state, const AppSettingsSceneContext* context); void totp_scene_app_settings_render(Canvas* const canvas, PluginState* plugin_state); -bool totp_scene_app_settings_handle_event(const PluginEvent* const event, PluginState* plugin_state); +bool totp_scene_app_settings_handle_event( + const PluginEvent* const event, + PluginState* plugin_state); void totp_scene_app_settings_deactivate(PluginState* plugin_state); void totp_scene_app_settings_free(const PluginState* plugin_state); \ No newline at end of file diff --git a/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.c b/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.c index c03f87542..652ca283b 100644 --- a/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.c +++ b/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.c @@ -73,7 +73,9 @@ void totp_scene_authenticate_render(Canvas* const canvas, PluginState* plugin_st } } -bool totp_scene_authenticate_handle_event(const PluginEvent* const event, PluginState* plugin_state) { +bool totp_scene_authenticate_handle_event( + const PluginEvent* const event, + PluginState* plugin_state) { if(event->type == EventTypeKey) { if(event->input.type == InputTypeLong && event->input.key == InputKeyBack) { return false; diff --git a/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.h b/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.h index aa308d983..c54152f62 100644 --- a/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.h +++ b/applications/plugins/totp/scenes/authenticate/totp_scene_authenticate.h @@ -9,6 +9,8 @@ void totp_scene_authenticate_init(PluginState* plugin_state); void totp_scene_authenticate_activate(PluginState* plugin_state); void totp_scene_authenticate_render(Canvas* const canvas, PluginState* plugin_state); -bool totp_scene_authenticate_handle_event(const PluginEvent* const event, PluginState* plugin_state); +bool totp_scene_authenticate_handle_event( + const PluginEvent* const event, + PluginState* plugin_state); void totp_scene_authenticate_deactivate(PluginState* plugin_state); void totp_scene_authenticate_free(const PluginState* plugin_state); diff --git a/applications/plugins/totp/services/ui/ui_controls.c b/applications/plugins/totp/services/ui/ui_controls.c index 8ed73498c..b01036f2e 100644 --- a/applications/plugins/totp/services/ui/ui_controls.c +++ b/applications/plugins/totp/services/ui/ui_controls.c @@ -5,7 +5,11 @@ #define TEXT_BOX_HEIGHT 13 #define TEXT_BOX_MARGIN 4 -void ui_control_text_box_render(Canvas* const canvas, int16_t y, const char* text, bool is_selected) { +void ui_control_text_box_render( + Canvas* const canvas, + int16_t y, + const char* text, + bool is_selected) { if(y < -TEXT_BOX_HEIGHT) { return; } diff --git a/applications/plugins/totp/services/ui/ui_controls.h b/applications/plugins/totp/services/ui/ui_controls.h index 02b278cf9..ef3af5f55 100644 --- a/applications/plugins/totp/services/ui/ui_controls.h +++ b/applications/plugins/totp/services/ui/ui_controls.h @@ -3,7 +3,11 @@ #include #include -void ui_control_text_box_render(Canvas* const canvas, int16_t y, const char* text, bool is_selected); +void ui_control_text_box_render( + Canvas* const canvas, + int16_t y, + const char* text, + bool is_selected); void ui_control_button_render( Canvas* const canvas, int16_t x, diff --git a/applications/plugins/totp/totp_app.c b/applications/plugins/totp/totp_app.c index 3bc85e211..f296a734b 100644 --- a/applications/plugins/totp/totp_app.c +++ b/applications/plugins/totp/totp_app.c @@ -24,7 +24,7 @@ static void render_callback(Canvas* const canvas, void* ctx) { PluginState* plugin_state = acquire_mutex((ValueMutex*)ctx, 25); - if (plugin_state != NULL && !plugin_state->changing_scene) { + if(plugin_state != NULL && !plugin_state->changing_scene) { totp_scene_director_render(canvas, plugin_state); } @@ -49,29 +49,43 @@ static bool totp_plugin_state_init(PluginState* const plugin_state) { totp_scene_director_init_scenes(plugin_state); - if (plugin_state->crypto_verify_data == NULL) { + if(plugin_state->crypto_verify_data == NULL) { DialogMessage* message = dialog_message_alloc(); dialog_message_set_buttons(message, "No", NULL, "Yes"); - dialog_message_set_text(message, "Would you like to setup PIN?", SCREEN_WIDTH_CENTER, SCREEN_HEIGHT_CENTER, AlignCenter, AlignCenter); + dialog_message_set_text( + message, + "Would you like to setup PIN?", + SCREEN_WIDTH_CENTER, + SCREEN_HEIGHT_CENTER, + AlignCenter, + AlignCenter); DialogMessageButton dialog_result = dialog_message_show(plugin_state->dialogs, message); dialog_message_free(message); - if (dialog_result == DialogMessageButtonRight) { + if(dialog_result == DialogMessageButtonRight) { totp_scene_director_activate_scene(plugin_state, TotpSceneAuthentication, NULL); } else { totp_crypto_seed_iv(plugin_state, NULL, 0); totp_scene_director_activate_scene(plugin_state, TotpSceneGenerateToken, NULL); } - } else if (plugin_state->pin_set) { + } else if(plugin_state->pin_set) { totp_scene_director_activate_scene(plugin_state, TotpSceneAuthentication, NULL); } else { totp_crypto_seed_iv(plugin_state, NULL, 0); - if (totp_crypto_verify_key(plugin_state)) { + if(totp_crypto_verify_key(plugin_state)) { totp_scene_director_activate_scene(plugin_state, TotpSceneGenerateToken, NULL); } else { - FURI_LOG_E(LOGGING_TAG, "Digital signature verification failed. Looks like conf file was created on another flipper and can't be used on any other"); + FURI_LOG_E( + LOGGING_TAG, + "Digital signature verification failed. Looks like conf file was created on another flipper and can't be used on any other"); DialogMessage* message = dialog_message_alloc(); dialog_message_set_buttons(message, "Exit", NULL, NULL); - dialog_message_set_text(message, "Digital signature verification failed", SCREEN_WIDTH_CENTER, SCREEN_HEIGHT_CENTER, AlignCenter, AlignCenter); + dialog_message_set_text( + message, + "Digital signature verification failed", + SCREEN_WIDTH_CENTER, + SCREEN_HEIGHT_CENTER, + AlignCenter, + AlignCenter); dialog_message_show(plugin_state->dialogs, message); dialog_message_free(message); return false; @@ -94,7 +108,7 @@ static void totp_plugin_state_free(PluginState* plugin_state) { ListNode* node = plugin_state->tokens_list; ListNode* tmp; - while (node != NULL) { + while(node != NULL) { tmp = node->next; TokenInfo* tokenInfo = node->data; token_info_free(tokenInfo); @@ -102,7 +116,7 @@ static void totp_plugin_state_free(PluginState* plugin_state) { node = tmp; } - if (plugin_state->crypto_verify_data != NULL) { + if(plugin_state->crypto_verify_data != NULL) { free(plugin_state->crypto_verify_data); } free(plugin_state); @@ -112,7 +126,7 @@ int32_t totp_app() { FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent)); PluginState* plugin_state = malloc(sizeof(PluginState)); - if (!totp_plugin_state_init(plugin_state)) { + if(!totp_plugin_state_init(plugin_state)) { FURI_LOG_E(LOGGING_TAG, "App state initialization failed\r\n"); totp_plugin_state_free(plugin_state); return 254; @@ -137,18 +151,20 @@ int32_t totp_app() { bool processing = true; uint32_t last_user_interaction_time = furi_get_tick(); while(processing) { - if (plugin_state->changing_scene) continue; + if(plugin_state->changing_scene) continue; FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100); PluginState* plugin_state_m = acquire_mutex_block(&state_mutex); if(event_status == FuriStatusOk) { - if (event.type == EventTypeKey) { + if(event.type == EventTypeKey) { last_user_interaction_time = furi_get_tick(); } processing = totp_scene_director_handle_event(&event, plugin_state_m); - } else if (plugin_state_m->pin_set && plugin_state_m->current_scene != TotpSceneAuthentication && furi_get_tick() - last_user_interaction_time > IDLE_TIMEOUT) { + } else if( + plugin_state_m->pin_set && plugin_state_m->current_scene != TotpSceneAuthentication && + furi_get_tick() - last_user_interaction_time > IDLE_TIMEOUT) { totp_scene_director_activate_scene(plugin_state_m, TotpSceneAuthentication, NULL); } From 3d25838a53d7798ccdd5b27e474eb143920a5dbd Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:23:02 +0300 Subject: [PATCH 11/14] update gps https://github.com/ezod/flipperzero-gps --- applications/plugins/gps_nmea_uart/.gitignore | 52 ------------------ applications/plugins/gps_nmea_uart/README.md | 24 +++++++- applications/plugins/gps_nmea_uart/gps.c | 48 ++++++++++------ applications/plugins/gps_nmea_uart/gps_uart.c | 18 ++++++ applications/plugins/gps_nmea_uart/gps_uart.h | 6 ++ applications/plugins/gps_nmea_uart/ui.png | Bin 0 -> 84291 bytes applications/plugins/gps_nmea_uart/wiring.png | Bin 0 -> 82441 bytes 7 files changed, 75 insertions(+), 73 deletions(-) delete mode 100644 applications/plugins/gps_nmea_uart/.gitignore create mode 100644 applications/plugins/gps_nmea_uart/ui.png create mode 100644 applications/plugins/gps_nmea_uart/wiring.png diff --git a/applications/plugins/gps_nmea_uart/.gitignore b/applications/plugins/gps_nmea_uart/.gitignore deleted file mode 100644 index c6127b38c..000000000 --- a/applications/plugins/gps_nmea_uart/.gitignore +++ /dev/null @@ -1,52 +0,0 @@ -# Prerequisites -*.d - -# Object files -*.o -*.ko -*.obj -*.elf - -# Linker output -*.ilk -*.map -*.exp - -# Precompiled Headers -*.gch -*.pch - -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -*.idb -*.pdb - -# Kernel Module Compile Results -*.mod* -*.cmd -.tmp_versions/ -modules.order -Module.symvers -Mkfile.old -dkms.conf diff --git a/applications/plugins/gps_nmea_uart/README.md b/applications/plugins/gps_nmea_uart/README.md index f10c3cc75..5e36dabda 100644 --- a/applications/plugins/gps_nmea_uart/README.md +++ b/applications/plugins/gps_nmea_uart/README.md @@ -1,14 +1,32 @@ # GPS for Flipper Zero [Original link](https://github.com/ezod/flipperzero-gps) +[Adafruit Ultimate GPS Breakout]. -A simple Flipper Zero application for NMEA 0183 serial GPS modules, such as the [Adafruit Ultimate GPS Breakout]. +![ui](ui.png) -Heavy lifting (NMEA parsing) provided by [minmea], which is included in this repository. +Heavy lifting (NMEA parsing) provided by [minmea], which is included in this +repository. ## Hardware Setup -Connect the GPS module to power and the USART using GPIO pins 9 (3.3V), 11 (GND), 13 (TX), and 14 (RX), as appropriate. +Connect the GPS module to power and the USART using GPIO pins 9 (3.3V), 11 +(GND), 13 (TX), and 14 (RX), as appropriate. + +![wiring](wiring.png) + + +## Contributing + +This project was a learning exercise and is more or less "complete" from my +perspective, but I will happily accept pull requests that improve and enhance +the functionality for others. + +Currently, the app only parses RMC and GGA sentences, and displays a subset of +the data that fits on the screen. The UART is also hard-coded to 9600 baud. +These limitations are largely driven by the GPS module I have to work with. A +more elaborate UI with scrolling or multiple screens, as well as a configurable +baud rate, may be useful for other GPS modules. [Adafruit Ultimate GPS Breakout]: https://www.adafruit.com/product/746 [minmea]: https://github.com/kosma/minmea diff --git a/applications/plugins/gps_nmea_uart/gps.c b/applications/plugins/gps_nmea_uart/gps.c index 3536b3f1c..1df4882b5 100644 --- a/applications/plugins/gps_nmea_uart/gps.c +++ b/applications/plugins/gps_nmea_uart/gps.c @@ -20,30 +20,42 @@ static void render_callback(Canvas* const canvas, void* context) { return; } + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 32, 8, AlignCenter, AlignBottom, "Latitude"); + canvas_draw_str_aligned(canvas, 96, 8, AlignCenter, AlignBottom, "Longitude"); + canvas_draw_str_aligned(canvas, 21, 30, AlignCenter, AlignBottom, "Course"); + canvas_draw_str_aligned(canvas, 64, 30, AlignCenter, AlignBottom, "Speed"); + canvas_draw_str_aligned(canvas, 107, 30, AlignCenter, AlignBottom, "Altitude"); + canvas_draw_str_aligned(canvas, 32, 52, AlignCenter, AlignBottom, "Satellites"); + canvas_draw_str_aligned(canvas, 96, 52, AlignCenter, AlignBottom, "Last Fix"); + canvas_set_font(canvas, FontSecondary); char buffer[64]; - snprintf(buffer, 64, "LAT: %f", (double)gps_uart->status.latitude); - canvas_draw_str_aligned(canvas, 10, 10, AlignLeft, AlignBottom, buffer); - snprintf(buffer, 64, "LON: %f", (double)gps_uart->status.longitude); - canvas_draw_str_aligned(canvas, 10, 20, AlignLeft, AlignBottom, buffer); + snprintf(buffer, 64, "%f", (double)gps_uart->status.latitude); + canvas_draw_str_aligned(canvas, 32, 18, AlignCenter, AlignBottom, buffer); + snprintf(buffer, 64, "%f", (double)gps_uart->status.longitude); + canvas_draw_str_aligned(canvas, 96, 18, AlignCenter, AlignBottom, buffer); + snprintf(buffer, 64, "%.1f", (double)gps_uart->status.course); + canvas_draw_str_aligned(canvas, 21, 40, AlignCenter, AlignBottom, buffer); + snprintf(buffer, 64, "%.2f kn", (double)gps_uart->status.speed); + canvas_draw_str_aligned(canvas, 64, 40, AlignCenter, AlignBottom, buffer); snprintf( buffer, 64, - "C/S: %.1f / %.2fkn", - (double)gps_uart->status.course, - (double)gps_uart->status.speed); - canvas_draw_str_aligned(canvas, 10, 30, AlignLeft, AlignBottom, buffer); - snprintf( - buffer, - 64, - "ALT: %.1f %c", + "%.1f %c", (double)gps_uart->status.altitude, - gps_uart->status.altitude_units); - canvas_draw_str_aligned(canvas, 10, 40, AlignLeft, AlignBottom, buffer); - snprintf(buffer, 64, "FIX: %d", gps_uart->status.fix_quality); - canvas_draw_str_aligned(canvas, 10, 50, AlignLeft, AlignBottom, buffer); - snprintf(buffer, 64, "SAT: %d", gps_uart->status.satellites_tracked); - canvas_draw_str_aligned(canvas, 10, 60, AlignLeft, AlignBottom, buffer); + tolower(gps_uart->status.altitude_units)); + canvas_draw_str_aligned(canvas, 107, 40, AlignCenter, AlignBottom, buffer); + snprintf(buffer, 64, "%d", gps_uart->status.satellites_tracked); + canvas_draw_str_aligned(canvas, 32, 62, AlignCenter, AlignBottom, buffer); + snprintf( + buffer, + 64, + "%02d:%02d:%02d UTC", + gps_uart->status.time_hours, + gps_uart->status.time_minutes, + gps_uart->status.time_seconds); + canvas_draw_str_aligned(canvas, 96, 62, AlignCenter, AlignBottom, buffer); release_mutex((ValueMutex*)context, gps_uart); } diff --git a/applications/plugins/gps_nmea_uart/gps_uart.c b/applications/plugins/gps_nmea_uart/gps_uart.c index 3a1152f28..52ba660bc 100644 --- a/applications/plugins/gps_nmea_uart/gps_uart.c +++ b/applications/plugins/gps_nmea_uart/gps_uart.c @@ -41,6 +41,11 @@ static void gps_uart_parse_nmea(GpsUart* gps_uart, char* line) { gps_uart->status.longitude = minmea_tocoord(&frame.longitude); gps_uart->status.speed = minmea_tofloat(&frame.speed); gps_uart->status.course = minmea_tofloat(&frame.course); + gps_uart->status.time_hours = frame.time.hours; + gps_uart->status.time_minutes = frame.time.minutes; + gps_uart->status.time_seconds = frame.time.seconds; + + notification_message_block(gps_uart->notifications, &sequence_blink_green_10); } } break; @@ -53,6 +58,11 @@ static void gps_uart_parse_nmea(GpsUart* gps_uart, char* line) { gps_uart->status.altitude_units = frame.altitude_units; gps_uart->status.fix_quality = frame.fix_quality; gps_uart->status.satellites_tracked = frame.satellites_tracked; + gps_uart->status.time_hours = frame.time.hours; + gps_uart->status.time_minutes = frame.time.minutes; + gps_uart->status.time_seconds = frame.time.seconds; + + notification_message_block(gps_uart->notifications, &sequence_blink_magenta_10); } } break; @@ -135,6 +145,11 @@ GpsUart* gps_uart_enable() { gps_uart->status.altitude_units = ' '; gps_uart->status.fix_quality = 0; gps_uart->status.satellites_tracked = 0; + gps_uart->status.time_hours = 0; + gps_uart->status.time_minutes = 0; + gps_uart->status.time_seconds = 0; + + gps_uart->notifications = furi_record_open(RECORD_NOTIFICATION); gps_uart->thread = furi_thread_alloc(); furi_thread_set_name(gps_uart->thread, "GpsUartWorker"); @@ -151,5 +166,8 @@ void gps_uart_disable(GpsUart* gps_uart) { furi_thread_flags_set(furi_thread_get_id(gps_uart->thread), WorkerEvtStop); furi_thread_join(gps_uart->thread); furi_thread_free(gps_uart->thread); + + furi_record_close(RECORD_NOTIFICATION); + free(gps_uart); } diff --git a/applications/plugins/gps_nmea_uart/gps_uart.h b/applications/plugins/gps_nmea_uart/gps_uart.h index 70a2db448..d6aafae9f 100644 --- a/applications/plugins/gps_nmea_uart/gps_uart.h +++ b/applications/plugins/gps_nmea_uart/gps_uart.h @@ -1,6 +1,7 @@ #pragma once #include +#include #define GPS_BAUDRATE 9600 #define RX_BUF_SIZE 1024 @@ -15,6 +16,9 @@ typedef struct { char altitude_units; int fix_quality; int satellites_tracked; + int time_hours; + int time_minutes; + int time_seconds; } GpsStatus; typedef struct { @@ -22,6 +26,8 @@ typedef struct { FuriStreamBuffer* rx_stream; uint8_t rx_buf[RX_BUF_SIZE]; + NotificationApp* notifications; + GpsStatus status; } GpsUart; diff --git a/applications/plugins/gps_nmea_uart/ui.png b/applications/plugins/gps_nmea_uart/ui.png new file mode 100644 index 0000000000000000000000000000000000000000..8e521457490e93d82928d1dfa1436b437978642a GIT binary patch literal 84291 zcmXtf1yoy2v^5lWcX#)o#oev6Kyh~|?q1y8DemsUiUui82~ynM-Tr*%^k@pv zF~ytToiEKAk2kk#sza+O5d(q*gany@@=+g3$ZV#<0_UJE--IqtC~yU1_Fp^%z#8BD zSXkm@EpnlD7#AY>e56ve#h)ls~>sAO3Z2Ia2p&aOf`oU z_#J;!Lna00E5WNw1p?XtbKvO}$xhJ(>BR1&vQK^{1Aa6at7n5IH}AH>c16`u>a>N( zHzFslMP}YoceVQgc|#*zOQA^<+Qvw_NKDG&aBk)Xtm3Z>!;;zeIHK=VV2)%ZdTk

KQky$* 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 literal 0 HcmV?d00001 diff --git a/applications/plugins/gps_nmea_uart/wiring.png b/applications/plugins/gps_nmea_uart/wiring.png new file mode 100644 index 0000000000000000000000000000000000000000..74b4a4401db683d4d5ce8917036c88302bad5cc1 GIT binary patch literal 82441 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ƛ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 literal 0 HcmV?d00001 From 8333ebd40100433a3d20355a80ad78815a3f69ec Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:41:56 +0300 Subject: [PATCH 12/14] new icons for subghz signals and infrared remotes by @Svaarich --- applications/main/subghz/views/receiver.c | 6 +++--- assets/icons/Infrared/Rotate_25x27.png | Bin 1685 -> 250 bytes assets/icons/Infrared/Rotate_hvr_25x27.png | Bin 1664 -> 237 bytes assets/icons/Infrared/Timer_25x27.png | Bin 1936 -> 313 bytes assets/icons/Infrared/Timer_hvr_25x27.png | Bin 1916 -> 306 bytes assets/icons/SubGhz/Dynamic_9x7.png | Bin 0 -> 129 bytes assets/icons/SubGhz/Raw_9x7.png | Bin 0 -> 127 bytes assets/icons/SubGhz/Static_9x7.png | Bin 0 -> 123 bytes 8 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 assets/icons/SubGhz/Dynamic_9x7.png create mode 100644 assets/icons/SubGhz/Raw_9x7.png create mode 100644 assets/icons/SubGhz/Static_9x7.png diff --git a/applications/main/subghz/views/receiver.c b/applications/main/subghz/views/receiver.c index 547b3c3fd..ebbcc2453 100644 --- a/applications/main/subghz/views/receiver.c +++ b/applications/main/subghz/views/receiver.c @@ -30,9 +30,9 @@ typedef struct SubGhzReceiverHistory SubGhzReceiverHistory; static const Icon* ReceiverItemIcons[] = { [SubGhzProtocolTypeUnknown] = &I_Quest_7x8, - [SubGhzProtocolTypeStatic] = &I_Unlock_7x8, - [SubGhzProtocolTypeDynamic] = &I_Lock_7x8, - [SubGhzProtocolTypeRAW] = &I_Unlock_7x8, + [SubGhzProtocolTypeStatic] = &I_Static_9x7, + [SubGhzProtocolTypeDynamic] = &I_Dynamic_9x7, + [SubGhzProtocolTypeRAW] = &I_Raw_9x7, }; typedef enum { diff --git a/assets/icons/Infrared/Rotate_25x27.png b/assets/icons/Infrared/Rotate_25x27.png index 0c62e9dc10bcccf575253199f68f724e72401f38..648634a093414fb8873757c2720bf5484972191f 100644 GIT binary patch delta 222 zcmV<403rXC4f+9)B!2;OQb$4nuFf3k0002ANklE6s7DtH)V6*Sws$&--Q5;>F(XebBNwtCvu5{r!@zMOUkSxnCJS{q3QlH(XY* z-@zOrU42??O!xf?{_lP7`f<3B@swTGN^%o#?!?j~H>Z#Qif7bakccqP!d+IehYSe7 Y2`a@}N9|Xfd;kCd07*qoM6N<$g7dLxqyPW_ literal 1685 zcmcIlU5MON6rM`eZLvQ1P_T-`yY&yf$<57VG6|h7>&)tm+ichEU{}QIoqKO);!bk2 zNp>f@P>|9WA4HM*^DYIS1W_M~5BjDRK@s}kf}$W+QB;)nK@h!}pID?__r-xsZj$eQ z=R4;+IeBhz;o*II59}?KO8Z*P#*#eWDv#Tyu9N>a)Zd&fV z-`tG(vCV}x-CUtI12cz}LzyE90-h2j3;JQ=WHm74b>up~tpQ~SNmpuMHV>#QyNgO) z#9XPU2vUTwV%e%*DVM9&2NaAj(vYR;2x9D@ienne_y=UPn0d}psfh z8w>{Oz)(fp)pXmoHH0;cp+vxBBTPvK!(@8G(BKJ;{V4TCsN{^KBi7OykV;1>1W`d7 zCgU>6f@v9vG+jk`N)w>#ZVwHD0-dCD>oUq%?^TJD_D00DC7y`2nDV)G9;VZiV2l=f zqqVp{%#=}$_jw>;i4>+6p`xBhMbZ=3$fI~%WD_~{n3Gh*Z6W#-qZTJD3dSl@?mg~@ zObn9yaz*0}yg^c41JX4NQ59mdEnPE_V_Nqk!$D{Sb!8tgl9HW)1@pa)oj_gJX@yBj zLdsi>8jzw?-)9chZ9*urAt`$uNLbl`wpV4)u#ja|P3l!EWT;E-ZwR`UmoMKxYFj3# z*`j(?XNEz6!l>hkI3Ti5{D5@17KL4){G+BW`XY7(lQlQ;ZkFsa zi>9AQ>o&&k$1%P-UiE!t*mMq|d9T+1%}wSEjBoo_sou_Xj%WuxE=jjBe*{Z}mkvnG zXS=e}|JhvHwZtc6eS7IUGyZ>+J}xWmk+93<;M72#jFyj>kty20{Te>IVjNB2a>nJ$ zVz|nSVm$LuQpa-m{cNwz zKYyp!{owie?-!HTFM`uQ-yXklrg!6yKYV-Xu_O4NC&3f!==uG>pZNH|#ccoiJ(vF2 zx@GIPr(XH>h4!iL+{sU)?>-@Cp!dy7Uq0%cn0ooo*BW2EJAe7|uCMO6`_8Z5dgjIa P!_k^sXuSF0)2IFhxL_T= diff --git a/assets/icons/Infrared/Rotate_hvr_25x27.png b/assets/icons/Infrared/Rotate_hvr_25x27.png index d2ab0f3fc5d73fff2744fc5d95bfa59ea0c2429a..a2b5cf93d22f53e2b09a9c14df3c16f7566f5c93 100644 GIT binary patch delta 220 zcmV<203-i^4ebGt8Gi-<004<0BxnEt00DDSM?wIu&K&6g005^+L_t(YiPe?c4!|G? zLyOt}|8n=RnHYwGw=D8VOpmPC2}$BD5y7e|WRZ3Nv|ic)&>#W8_R;xD{*gl!EITJw zS_$DoNV-Nx79liAO(^=}-Tz7CrbF2kg-xaK>N;nIUC2ll^%Tixl3k#zn{;C}003?v W?AKH z*_qk(U=g9{LG<2}P!JRq1&gd=y{I69AVM!f5%D66^desT^JV^7q+R!7AjwPK@B4kf z?@zwGvbOrfsnSCwMNv+*R+{T@e-O@xPu>OByBi6dT8J+4u?k39G&IM1Mo7{dt2hHy_*PQU1V}6Wny_C;=DX_#@XC9*-883 zNI5e^Uqk@QKv-q&D(T5wW<7a_IOf~)XcDiH3YyEbEoFaJsI?i3N{Eitg^e)ga+p0* zD4H6GCe4MfLDdMt4kFbyRIN%pv-T)9J&Y&N0Q!K_oE{A6EtPAX$lI;}fE9@t2%OwMjd4@cW ziCLwzgRtFU$f;Twxt%ITwT`fu;|Ry8)Mji?r6GJdw0|pWp^5{e%UxSw+eRH>f*{v2 z5oLrS+v3=AD8@GCn#!5CCDVvPpM(+Z3L}ZTn);8LhV08UkQ{Pu7TpZ&5XDNELFsm< z-^Y3J{B+e1)p65#loh@1Ypf`;06&x0;cc>aG@TRsVNU?*A;q7-GP#uxX(|@Gkm>(y zF5^z(Gdeh2`q2viKT4mbmGx-c6)-prtq8^_M$Dur#=q+|-n(TS&EPQOisgK`!oz$# zix{X=7``8U`)3OV_p`0$VteJwgYPet6!qlsYoGpg<=}1d<_AwNyyaiJao@{7T?`($ z{QA!~E|o6b^WA$(?dvD5mddZ9PnLh%Kl8b`d09Mu`kf#4KmOuJV?e%n|CiG*+*~T( kUw`Aph2QJv{x~@P+UC`bi(i+H75|9V@@n(lrRQGy8<+49YXATM diff --git a/assets/icons/Infrared/Timer_25x27.png b/assets/icons/Infrared/Timer_25x27.png index 5ce468198d2d83d7a20845f491fc2495fd8b6d46..2f1853a349d2f01770883a2bc1d9d14469c31e4d 100644 GIT binary patch delta 297 zcmV+^0oMMI54i%68Gi-<004<0BxnEt00DDSM?wIu&K&6g008qzL_t(YiLF-462l+} zgpB|H%k)w*s{$?)Iab&QC8(O2$v;$8ZUw-~Kmt7Y8i+Sog2-i6V`eYz=s3VUrnOEq zj+ue7RoF_nihfv<6(|DEI&yU4U=DyT@NCVaNU9=K&*X;fVt)uKGK$0t*|9xM=thDC zT_>5a<8-O&=XEoYM7FDbM*<=@)OQjQb%tXbVFW?|SD2kwE!{|%r5EK@g!JxWL}W4j zC)o&gA{rigL4Jvi1l_Q%Fh6Q%>Le@3R!t=+Bsnd-jJSQDFl(IsAXP++N^CwPutv?y vy7vbY)2-=`yCRzpI}aEAtUuf)5E1zSP+Q6_iB2Ro00000NkvXXu0mjfv;KPO literal 1936 zcmcIlTWB0r7+x)EEk?BVr7hwxc`>vzbLO%;7rG7EOS@~`G^PuQK6&PxGrL1}=1gWL z*-fY*)fUnR#RqMRC<+buP%NYr3`J@|TTo~PFQxRs`k)UMsZWKxsAu+;C~3@#3$t_k z&i8-k`@cD7VQlo^z_wl6QmNEHxl|Zu=bh}>z4c-C|7iZgFgraSlwOWfDRD449%?NP z&$3q^xz)*dvht#Zs3$^9Z6db3fT2^VeZ#E)qA3z{HmSLOmcMuDGS9g<%TJ^$pc3Rs z-7U?BdF6V~rOMbZfD@EMJe~ zz>=h9vne)Jk%l!%Hce9kilit4LkQ8FAH$a5M}s|v0*O%Q2C++hE@6Z=or$wNQ`(t= z7xZcUsB055OlrYEl0}eAsRyi7Himj$A05TTSyrX1cU|JBIu{UWoJ4dcM5H)N{CKce zEU43%Ms@lCZT;8(P>4~#U@#Ll+V)^1kp}S?cf_!ATVG)1Ljq$ORw-@t8a39d$SH~l zxaTKaAJb+ulql*BAO#qcEYG^82*41OOjXt`Rk!5K9-vwPbf6WM0}NxhIWWbpGq)LN zrDB!+D26^F8HXU0)j)nz8WjTPRW&@x)HiAxD7aL!oXeMzY z8Q+O5rigL$-Lx`}4NM(5ERGo>IEH2lFr9V;fDKhqk*=FW>9HLSUH0X`##+`y6=sYi ztpP_xX+hUjQ^1a<2vAYef}#Q>Lro<*LOi!xH%}WhtWeC{Re260%b}qMS$Y;%{pPE8#KQI zi>MPfVMs=5%<2COm-Ha<5uDvv`sNz{KT7Y~it5m>5w>tjd@>m+SzlsDUypgtjgK4FW+`HA66EjR+Xq4pw!bn|7w7qT#M`^@6E^ zT+U3(xx87_Wx1F$jlxLrfC7qoTFDm;W6j9+Oge(3jg_~NYnIub4Q%aBF8iAXyXkK` z#AlX;Y*RUPXZjnqA-z{Fj8se4K7aSn_SeDI{PJImcfbDg#G9vQ@#2o3QdhSBelhT# zvOZb<@$$fvxuLV4-PyTt?&+0hrmlJ)(!FP25qaC(a%Jg*w|;%>`%7QGf8wOPeCW)L zW5-|k?yKYW?(;7>*WW(5tNinCN8g#=bNl?#j-k8Z(P!!Po4+g{TsigHANLk+G(Rrg sdgJz)M^=WP_;%^J!;M=PC+^?havJ=A$hj|WCVOVNI9m8L_v*2~0cQewH~;_u diff --git a/assets/icons/Infrared/Timer_hvr_25x27.png b/assets/icons/Infrared/Timer_hvr_25x27.png index f6959c24476f22bde7e1992727ca7e62802d23e2..d4dffa54465733c6db363e93f134feabaae586ff 100644 GIT binary patch delta 290 zcmV+-0p0%m4zdD}8Gi-<004<0BxnEt00DDSM?wIu&K&6g008VsL_t(YiM3Ws7Q`S3 z!#?kS<;-GgA0nUFak3Q>Kw{efKyI|wlWTe4Q20m3c&w5CG{r9q3q3G|Fir z!bRW2qq&2pgj>`67j0Y=Is^WjhXU^C$zI1)16e#&Cav4#V3_7q7E{_((yY*M40X* z^2QEct?&sfBDx#4D;@8RGwdWvh@P6PAXlV%@FL;u?+NpavxcNUzi)mdo;UzF{X3)q o;Cm;(dLt7MQ5zS3T1fMK0P>8kGw&XkfB*mh07*qoM6N<$f(T)L2><{9 literal 1916 zcmcIlTWB0r7~Xg(u~0!mDXj>@2EBYWJ63^@{QPP+f7iQfFV=_-OS2)O*4!q1qCWlg1RI_pePp1TRBze|3n@wBCfSqneNBpPX)1=B%viq z?RGocRNU~{~5>zBb0UQD1jUYiC5X9pHh6;^w)(L80ac5d}rbLgDn1 z7ZBEt&!md_6KDk`v>@`TDF7Nk$=7AwQVmO0&qCFLum`ny9}twF!+{y`+>OIPZQGg+ z;sgbl&Q=N{7nSuqVmYR)sU|dmi>Lu)r0KxHE(KI}WF_aQhNC-OU3`9p;kDF>bbhaG z3B#OY=%$s+5zSE4JU}W|0f8C;rU5Z5zeU7oMIw3 z4=}|ppjdN&K@kO7-bKhXRoQ@=DD1{9vlfeNM!37GC=hJ9Ebnja@8>Q%koZ;Dx()c1|vhy!p}xH|m9tpZ)C4*YeIA+b{n)^27A#@ryIU QSb9FrR_7|)r3+hs122+kE&u=k diff --git a/assets/icons/SubGhz/Dynamic_9x7.png b/assets/icons/SubGhz/Dynamic_9x7.png new file mode 100644 index 0000000000000000000000000000000000000000..50922d4fb5c8c2511750e371b087ce0d09dfeb3f GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRZ!3HF6%}!4MQjEnx?oJHr&dIz4a;!aF9780g z_VyWa9WW3$yyVaS^REK(I5d~c>zUZQ_R%6!H& ce1{YN)+kB#6I1hifW|O*y85}Sb4q9e0Qu4==l}o! literal 0 HcmV?d00001 diff --git a/assets/icons/SubGhz/Raw_9x7.png b/assets/icons/SubGhz/Raw_9x7.png new file mode 100644 index 0000000000000000000000000000000000000000..39a6d0386406e6f91b7dfd5a414aa43f8984fa6a GIT binary patch literal 127 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRZ!3HF6%}!4MQjEnx?oJHr&dIz4ax6Vv9780g z_Vzh)F(~jHzVz__ej9zQRAH0lQ~QKc(>6_LIqLkQ`LU%&#m@H?YZ*9R2(=ap=W^M5 Z3%kGI5ij$p;07AP;OXk;vd$@?2>?g7CO7~9 literal 0 HcmV?d00001 diff --git a/assets/icons/SubGhz/Static_9x7.png b/assets/icons/SubGhz/Static_9x7.png new file mode 100644 index 0000000000000000000000000000000000000000..dad4833e392718fe05fd50e4fe514bfa660aa5a8 GIT binary patch literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRZ!3HF6%}!4MQjEnx?oJHr&dIz4a!fs49780g z_VyX_F(~jb@n8O5FDcI6Y Date: Fri, 28 Oct 2022 19:43:54 +0300 Subject: [PATCH 13/14] [FL-2911] IR Universal Audio Remote (#1942) * Add Audio universal remote * Add signal library for Audio Universal Remote * Update UniversalRemotes.md * Added IR profile for Samsung K450 soundbar (#1892) * Add symbols to API file * Rearrange Audio remote buttons * Add new icons, remove old ones * Remove old signals, add new ones * Add universal audio remote to CLI, refactor code * Improve help text * Correct formatting * Update UniversalRemotes.md * Furi: restore correct api_symbols.csv version Co-authored-by: Alexei Humeniy Co-authored-by: Aleksandr Kutuzov --- applications/main/infrared/infrared_cli.c | 211 ++++++--------- .../infrared/scenes/infrared_scene_config.h | 1 + .../scenes/infrared_scene_universal.c | 8 +- .../scenes/infrared_scene_universal_audio.c | 133 ++++++++++ assets/icons/Infrared/Pause_25x27.png | Bin 0 -> 3634 bytes assets/icons/Infrared/Pause_hvr_25x27.png | Bin 0 -> 3623 bytes assets/icons/Infrared/Play_25x27.png | Bin 0 -> 3653 bytes assets/icons/Infrared/Play_hvr_25x27.png | Bin 0 -> 3643 bytes assets/icons/Infrared/TrackNext_25x27.png | Bin 0 -> 3651 bytes assets/icons/Infrared/TrackNext_hvr_25x27.png | Bin 0 -> 3639 bytes assets/icons/Infrared/TrackPrev_25x27.png | Bin 0 -> 3657 bytes assets/icons/Infrared/TrackPrev_hvr_25x27.png | Bin 0 -> 3644 bytes assets/resources/infrared/assets/audio.ir | 244 ++++++++++++++++++ documentation/UniversalRemotes.md | 16 +- 14 files changed, 486 insertions(+), 127 deletions(-) create mode 100644 applications/main/infrared/scenes/infrared_scene_universal_audio.c create mode 100644 assets/icons/Infrared/Pause_25x27.png create mode 100644 assets/icons/Infrared/Pause_hvr_25x27.png create mode 100644 assets/icons/Infrared/Play_25x27.png create mode 100644 assets/icons/Infrared/Play_hvr_25x27.png create mode 100644 assets/icons/Infrared/TrackNext_25x27.png create mode 100644 assets/icons/Infrared/TrackNext_hvr_25x27.png create mode 100644 assets/icons/Infrared/TrackPrev_25x27.png create mode 100644 assets/icons/Infrared/TrackPrev_hvr_25x27.png create mode 100644 assets/resources/infrared/assets/audio.ir diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index 5a04f7495..8f35a8fd1 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -5,25 +5,21 @@ #include #include #include +#include #include "infrared_signal.h" #include "infrared_brute_force.h" -#include - #define INFRARED_CLI_BUF_SIZE 10 +#define INFRARED_ASSETS_FOLDER "infrared/assets" +#define INFRARED_BRUTE_FORCE_DUMMY_INDEX 0 DICT_DEF2(dict_signals, FuriString*, FURI_STRING_OPLIST, int, M_DEFAULT_OPLIST) -enum RemoteTypes { TV = 0, AC = 1 }; - static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args); static void infrared_cli_start_ir_tx(Cli* cli, FuriString* args); static void infrared_cli_process_decode(Cli* cli, FuriString* args); static void infrared_cli_process_universal(Cli* cli, FuriString* args); -static void infrared_cli_list_remote_signals(enum RemoteTypes remote_type); -static void - infrared_cli_brute_force_signals(Cli* cli, enum RemoteTypes remote_type, FuriString* signal); static const struct { const char* cmd; @@ -87,8 +83,10 @@ static void infrared_cli_print_usage(void) { INFRARED_MIN_FREQUENCY, INFRARED_MAX_FREQUENCY); printf("\tir decode []\r\n"); - printf("\tir universal \r\n"); - printf("\tir universal list \r\n"); + printf("\tir universal \r\n"); + printf("\tir universal list \r\n"); + // TODO: Do not hardcode universal remote names + printf("\tAvailable universal remotes: tv audio ac\r\n"); } static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args) { @@ -356,89 +354,31 @@ static void infrared_cli_process_decode(Cli* cli, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void infrared_cli_process_universal(Cli* cli, FuriString* args) { - enum RemoteTypes Remote; - - FuriString* command; - FuriString* remote; - FuriString* signal; - command = furi_string_alloc(); - remote = furi_string_alloc(); - signal = furi_string_alloc(); - - do { - if(!args_read_string_and_trim(args, command)) { - infrared_cli_print_usage(); - break; - } - - if(furi_string_cmp_str(command, "list") == 0) { - args_read_string_and_trim(args, remote); - if(furi_string_cmp_str(remote, "tv") == 0) { - Remote = TV; - } else if(furi_string_cmp_str(remote, "ac") == 0) { - Remote = AC; - } else { - printf("Invalid remote type.\r\n"); - break; - } - infrared_cli_list_remote_signals(Remote); - break; - } - - if(furi_string_cmp_str(command, "tv") == 0) { - Remote = TV; - } else if(furi_string_cmp_str(command, "ac") == 0) { - Remote = AC; - } else { - printf("Invalid remote type.\r\n"); - break; - } - - args_read_string_and_trim(args, signal); - if(furi_string_empty(signal)) { - printf("Must supply a valid signal for type of remote selected.\r\n"); - break; - } - - infrared_cli_brute_force_signals(cli, Remote, signal); - break; - - } while(false); - - furi_string_free(command); - furi_string_free(remote); - furi_string_free(signal); -} - -static void infrared_cli_list_remote_signals(enum RemoteTypes remote_type) { - Storage* storage = furi_record_open(RECORD_STORAGE); - FlipperFormat* ff = flipper_format_buffered_file_alloc(storage); - dict_signals_t signals_dict; - FuriString* key; - const char* remote_file = NULL; - bool success = false; - int max = 1; - - switch(remote_type) { - case TV: - remote_file = EXT_PATH("infrared/assets/tv.ir"); - break; - case AC: - remote_file = EXT_PATH("infrared/assets/ac.ir"); - break; - default: - break; +static void infrared_cli_list_remote_signals(FuriString* remote_name) { + if(furi_string_empty(remote_name)) { + printf("Missing remote name.\r\n"); + return; } - dict_signals_init(signals_dict); - key = furi_string_alloc(); + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* ff = flipper_format_buffered_file_alloc(storage); + FuriString* remote_path = furi_string_alloc_printf( + "%s/%s.ir", EXT_PATH(INFRARED_ASSETS_FOLDER), furi_string_get_cstr(remote_name)); + + do { + if(!flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(remote_path))) { + printf("Invalid remote name.\r\n"); + break; + } + + dict_signals_t signals_dict; + dict_signals_init(signals_dict); + + FuriString* key = furi_string_alloc(); + FuriString* signal_name = furi_string_alloc(); - success = flipper_format_buffered_file_open_existing(ff, remote_file); - if(success) { - FuriString* signal_name; - signal_name = furi_string_alloc(); printf("Valid signals:\r\n"); + int max = 1; while(flipper_format_read_string(ff, "name", signal_name)) { furi_string_set_str(key, furi_string_get_cstr(signal_name)); int* v = dict_signals_get(signals_dict, key); @@ -449,57 +389,57 @@ static void infrared_cli_list_remote_signals(enum RemoteTypes remote_type) { dict_signals_set_at(signals_dict, key, 1); } } + dict_signals_it_t it; for(dict_signals_it(it, signals_dict); !dict_signals_end_p(it); dict_signals_next(it)) { const struct dict_signals_pair_s* pair = dict_signals_cref(it); printf("\t%s\r\n", furi_string_get_cstr(pair->key)); } - furi_string_free(signal_name); - } - furi_string_free(key); - dict_signals_clear(signals_dict); + furi_string_free(key); + furi_string_free(signal_name); + dict_signals_clear(signals_dict); + + } while(false); + flipper_format_free(ff); + furi_string_free(remote_path); furi_record_close(RECORD_STORAGE); } static void - infrared_cli_brute_force_signals(Cli* cli, enum RemoteTypes remote_type, FuriString* signal) { + infrared_cli_brute_force_signals(Cli* cli, FuriString* remote_name, FuriString* signal_name) { InfraredBruteForce* brute_force = infrared_brute_force_alloc(); - const char* remote_file = NULL; - uint32_t i = 0; - bool success = false; + FuriString* remote_path = furi_string_alloc_printf( + "%s/%s.ir", EXT_PATH(INFRARED_ASSETS_FOLDER), furi_string_get_cstr(remote_name)); - switch(remote_type) { - case TV: - remote_file = EXT_PATH("infrared/assets/tv.ir"); - break; - case AC: - remote_file = EXT_PATH("infrared/assets/ac.ir"); - break; - default: - break; - } + infrared_brute_force_set_db_filename(brute_force, furi_string_get_cstr(remote_path)); + infrared_brute_force_add_record( + brute_force, INFRARED_BRUTE_FORCE_DUMMY_INDEX, furi_string_get_cstr(signal_name)); - infrared_brute_force_set_db_filename(brute_force, remote_file); - infrared_brute_force_add_record(brute_force, i++, furi_string_get_cstr(signal)); - - success = infrared_brute_force_calculate_messages(brute_force); - if(success) { - uint32_t record_count; - uint32_t index = 0; - int records_sent = 0; - bool running = false; - - running = infrared_brute_force_start(brute_force, index, &record_count); - if(record_count <= 0) { - printf("Invalid signal.\n"); - infrared_brute_force_reset(brute_force); - return; + do { + if(furi_string_empty(signal_name)) { + printf("Missing signal name.\r\n"); + break; + } + if(!infrared_brute_force_calculate_messages(brute_force)) { + printf("Invalid remote name.\r\n"); + break; } - printf("Sending %ld codes to the tv.\r\n", record_count); + uint32_t record_count; + bool running = infrared_brute_force_start( + brute_force, INFRARED_BRUTE_FORCE_DUMMY_INDEX, &record_count); + + if(record_count <= 0) { + printf("Invalid signal name.\r\n"); + break; + } + + printf("Sending %ld signal(s)...\r\n", record_count); printf("Press Ctrl-C to stop.\r\n"); + + int records_sent = 0; while(running) { running = infrared_brute_force_send_next(brute_force); @@ -510,14 +450,35 @@ static void } infrared_brute_force_stop(brute_force); - } else { - printf("Invalid signal.\r\n"); - } + } while(false); + furi_string_free(remote_path); infrared_brute_force_reset(brute_force); infrared_brute_force_free(brute_force); } +static void infrared_cli_process_universal(Cli* cli, FuriString* args) { + FuriString* arg1 = furi_string_alloc(); + FuriString* arg2 = furi_string_alloc(); + + do { + if(!args_read_string_and_trim(args, arg1)) break; + if(!args_read_string_and_trim(args, arg2)) break; + } while(false); + + if(furi_string_empty(arg1)) { + printf("Wrong arguments.\r\n"); + infrared_cli_print_usage(); + } else if(furi_string_equal_str(arg1, "list")) { + infrared_cli_list_remote_signals(arg2); + } else { + infrared_cli_brute_force_signals(cli, arg1, arg2); + } + + furi_string_free(arg1); + furi_string_free(arg2); +} + static void infrared_cli_start_ir(Cli* cli, FuriString* args, void* context) { UNUSED(context); if(furi_hal_infrared_is_busy()) { diff --git a/applications/main/infrared/scenes/infrared_scene_config.h b/applications/main/infrared/scenes/infrared_scene_config.h index 22125fb79..111fd2d31 100644 --- a/applications/main/infrared/scenes/infrared_scene_config.h +++ b/applications/main/infrared/scenes/infrared_scene_config.h @@ -16,6 +16,7 @@ ADD_SCENE(infrared, remote_list, RemoteList) ADD_SCENE(infrared, universal, Universal) ADD_SCENE(infrared, universal_tv, UniversalTV) ADD_SCENE(infrared, universal_ac, UniversalAC) +ADD_SCENE(infrared, universal_audio, UniversalAudio) ADD_SCENE(infrared, debug, Debug) ADD_SCENE(infrared, error_databases, ErrorDatabases) ADD_SCENE(infrared, rpc, Rpc) diff --git a/applications/main/infrared/scenes/infrared_scene_universal.c b/applications/main/infrared/scenes/infrared_scene_universal.c index 2bd7082c4..914360d78 100644 --- a/applications/main/infrared/scenes/infrared_scene_universal.c +++ b/applications/main/infrared/scenes/infrared_scene_universal.c @@ -21,6 +21,12 @@ void infrared_scene_universal_on_enter(void* context) { SubmenuIndexUniversalTV, infrared_scene_universal_submenu_callback, context); + submenu_add_item( + submenu, + "Audio Players", + SubmenuIndexUniversalAudio, + infrared_scene_universal_submenu_callback, + context); submenu_add_item( submenu, "Air Conditioners", @@ -45,7 +51,7 @@ bool infrared_scene_universal_on_event(void* context, SceneManagerEvent event) { scene_manager_next_scene(scene_manager, InfraredSceneUniversalAC); consumed = true; } else if(event.event == SubmenuIndexUniversalAudio) { - //TODO Implement Audio universal remote + scene_manager_next_scene(scene_manager, InfraredSceneUniversalAudio); consumed = true; } } diff --git a/applications/main/infrared/scenes/infrared_scene_universal_audio.c b/applications/main/infrared/scenes/infrared_scene_universal_audio.c new file mode 100644 index 000000000..00c86fff4 --- /dev/null +++ b/applications/main/infrared/scenes/infrared_scene_universal_audio.c @@ -0,0 +1,133 @@ +#include "../infrared_i.h" + +#include "common/infrared_scene_universal_common.h" + +void infrared_scene_universal_audio_on_enter(void* context) { + infrared_scene_universal_common_on_enter(context); + + Infrared* infrared = context; + ButtonPanel* button_panel = infrared->button_panel; + InfraredBruteForce* brute_force = infrared->brute_force; + + infrared_brute_force_set_db_filename(brute_force, EXT_PATH("infrared/assets/audio.ir")); + + button_panel_reserve(button_panel, 2, 4); + uint32_t i = 0; + button_panel_add_item( + button_panel, + i, + 0, + 0, + 3, + 11, + &I_Power_25x27, + &I_Power_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Power"); + button_panel_add_item( + button_panel, + i, + 1, + 0, + 36, + 11, + &I_Mute_25x27, + &I_Mute_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Mute"); + button_panel_add_item( + button_panel, + i, + 0, + 1, + 3, + 41, + &I_Play_25x27, + &I_Play_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Play"); + button_panel_add_item( + button_panel, + i, + 1, + 1, + 36, + 41, + &I_Pause_25x27, + &I_Pause_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Pause"); + button_panel_add_item( + button_panel, + i, + 0, + 2, + 3, + 71, + &I_TrackPrev_25x27, + &I_TrackPrev_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Prev"); + button_panel_add_item( + button_panel, + i, + 1, + 2, + 36, + 71, + &I_TrackNext_25x27, + &I_TrackNext_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Next"); + button_panel_add_item( + button_panel, + i, + 0, + 3, + 3, + 101, + &I_Vol_down_25x27, + &I_Vol_down_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Vol_dn"); + button_panel_add_item( + button_panel, + i, + 1, + 3, + 36, + 101, + &I_Vol_up_25x27, + &I_Vol_up_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Vol_up"); + + button_panel_add_label(button_panel, 1, 8, FontPrimary, "Mus. remote"); + + view_set_orientation(view_stack_get_view(infrared->view_stack), ViewOrientationVertical); + view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewStack); + + infrared_show_loading_popup(infrared, true); + bool success = infrared_brute_force_calculate_messages(brute_force); + infrared_show_loading_popup(infrared, false); + + if(!success) { + scene_manager_next_scene(infrared->scene_manager, InfraredSceneErrorDatabases); + } +} + +bool infrared_scene_universal_audio_on_event(void* context, SceneManagerEvent event) { + return infrared_scene_universal_common_on_event(context, event); +} + +void infrared_scene_universal_audio_on_exit(void* context) { + infrared_scene_universal_common_on_exit(context); +} diff --git a/assets/icons/Infrared/Pause_25x27.png b/assets/icons/Infrared/Pause_25x27.png new file mode 100644 index 0000000000000000000000000000000000000000..a371ba81718e864efe908e61f34bc842079cc20d GIT binary patch literal 3634 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 literal 0 HcmV?d00001 diff --git a/assets/icons/Infrared/Pause_hvr_25x27.png b/assets/icons/Infrared/Pause_hvr_25x27.png new file mode 100644 index 0000000000000000000000000000000000000000..472d583db0ff9adc5ef810675c8b915c6e5a899c GIT binary patch literal 3623 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 HpN{x9W3^Ap literal 0 HcmV?d00001 diff --git a/assets/icons/Infrared/Play_hvr_25x27.png b/assets/icons/Infrared/Play_hvr_25x27.png new file mode 100644 index 0000000000000000000000000000000000000000..6708dcdbf2c7ac0c656bbb1c2441fb0f4d6fc9c5 GIT binary patch literal 3643 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 SxG~3f0odCdwl1|i74;vAA8iN# literal 0 HcmV?d00001 diff --git a/assets/icons/Infrared/TrackNext_hvr_25x27.png b/assets/icons/Infrared/TrackNext_hvr_25x27.png new file mode 100644 index 0000000000000000000000000000000000000000..a4de4fc3cbe582349b8c79bbb7ec6e931e8bc792 GIT binary patch literal 3639 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 W@<`O{ko&lf02|BW7A0mL7ybo9V{1nM literal 0 HcmV?d00001 diff --git a/assets/icons/Infrared/TrackPrev_hvr_25x27.png b/assets/icons/Infrared/TrackPrev_hvr_25x27.png new file mode 100644 index 0000000000000000000000000000000000000000..838055341e568fed60b3e28a280fe839d2c9db9e GIT binary patch literal 3644 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 in order to keep the library organised. + +When done, open a pull request containing the changed file. + ## Air Conditioners ### Recording signals Air conditioners differ from most other infrared-controlled devices because their state is tracked by the remote. @@ -31,7 +45,7 @@ Finally, record the `Off` signal: 4. Save the resulting signal under the name `Off`. The resulting remote file should now contain 6 signals. Any of them can be omitted, but that will mean that this functionality will not be used. -Test the file against the actual device. Every signal must do what it's supposed to. +Test the file against the actual device. Make sure that every signal does what it's supposed to. If everything checks out, append these signals **to the end** of the [A/C universal remote file](/assets/resources/infrared/assets/ac.ir). From cb47edf82f87e71fd6b05bf44d1430d09401ca50 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:45:59 +0300 Subject: [PATCH 14/14] rm unused file --- .github/workflows/unit_tests.yml | 52 -------------------------------- 1 file changed, 52 deletions(-) delete mode 100644 .github/workflows/unit_tests.yml diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml deleted file mode 100644 index b5bf10004..000000000 --- a/.github/workflows/unit_tests.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: 'Unit tests' - -on: - pull_request: - -env: - TARGETS: f7 - DEFAULT_TARGET: f7 - -jobs: - main: - runs-on: [self-hosted, FlipperZeroTest] - steps: - - name: Checkout code - uses: actions/checkout@v2 - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - - name: 'Get flipper from device manager (mock)' - id: device - run: | - echo "flipper=/dev/ttyACM0" >> $GITHUB_OUTPUT - - - name: 'Compile unit tests firmware' - id: compile - run: | - FBT_TOOLCHAIN_PATH=/opt ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1 - - - name: 'Wait for flipper to finish updating' - id: connect - if: steps.compile.outcome == 'success' - run: | - python3 ./scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}} - - - name: 'Format flipper SD card' - id: format - if: steps.connect.outcome == 'success' - run: | - ./scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext - - - name: 'Copy assets and unit tests data to flipper' - id: copy - if: steps.format.outcome == 'success' - run: | - ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/resources /ext - ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/unit_tests /ext/unit_tests - - - name: 'Run units and validate results' - if: steps.copy.outcome == 'success' - run: | - python3 ./scripts/testing/units.py ${{steps.device.outputs.flipper}}