Compare commits

..

6 Commits

Author SHA1 Message Date
Andrea Santaniello
dc0f30dad9 Update psa.c
All checks were successful
Build Dev Firmware / build (push) Successful in 6m46s
2026-03-29 14:34:00 +02:00
Andrea
38f261e23b Revert "Make psa more strict to avoid false positives"
All checks were successful
Build Dev Firmware / build (push) Successful in 6m43s
This reverts commit cb1daaa4f1.
2026-03-28 21:44:11 +01:00
Andrea Santaniello
cb1daaa4f1 Make psa more strict to avoid false positives
All checks were successful
Build Dev Firmware / build (push) Successful in 7m8s
2026-03-28 18:31:50 +01:00
Andrea Santaniello
b318b3e9ff CRC check for marelli 2026-03-28 18:24:02 +01:00
D4rk$1d3
117381e5a1 Update README.md
All checks were successful
Build Dev Firmware / build (push) Successful in 7m2s
2026-03-25 14:28:18 -03:00
d4rks1d33
702cf5abc8 Update API, Multipage handling for all starline and scherkhan buttons (Still WIP), add ChiefCooker/CanCommander and CanTool as external apps
All checks were successful
Build Dev Firmware / build (push) Successful in 6m46s
2026-03-24 21:58:41 -03:00
153 changed files with 22203 additions and 53 deletions

View File

@@ -49,7 +49,8 @@ This project may incorporate, adapt, or build upon **other open-source projects*
| PSA (Peugeot/Citroën/DS) | PSA GROUP | 433 MHz | AM/FM | Yes | Yes | Yes |
| Ford | Ford V0 | 315/433 MHz | AM | Yes | Yes | Yes |
| Fiat | Fiat SpA | 433 MHz | AM | Yes | Yes | Yes |
| Fiat | Marelli/Delphi | 433 MHz | AM | No | Yes | No |
| Fiat | Marelli/Delphi | 433 MHz | AM | No | Yes | Yes |
| Renault (old models) | Marelli | 433 MHz | AM | No | Yes | No|
| Mazda | Siemens (5WK49365D) | 315/433 MHz | AM/FM | Yes | Yes | Yes |
| Kia/Hyundai | KIA/HYU V0 | 433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | KIA/HYU V1 | 315/433 MHz | AM | Yes | Yes | Yes |

View File

@@ -58,6 +58,7 @@ typedef enum {
SubGhzCustomEventViewTransmitterSendStart,
SubGhzCustomEventViewTransmitterSendStop,
SubGhzCustomEventViewTransmitterError,
SubGhzCustomEventViewTransmitterPageChange,
SubGhzCustomEventViewFreqAnalOkShort,
SubGhzCustomEventViewFreqAnalOkLong,

View File

@@ -94,6 +94,10 @@ bool subghz_scene_transmitter_on_event(void* context, SceneManagerEvent event) {
scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneStart);
return true;
} else if(event.event == SubGhzCustomEventViewTransmitterPageChange) {
// Page changed via OK button, refresh display
subghz_scene_transmitter_update_data_show(subghz);
return true;
} else if(event.event == SubGhzCustomEventViewTransmitterError) {
furi_string_set(subghz->error_str, "Protocol not\nfound!");
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowErrorSub);

View File

@@ -155,39 +155,68 @@ bool subghz_view_transmitter_input(InputEvent* event, void* context) {
true);
if(can_be_sent) {
// Long press d-pad: set custom btn + long flag (no send here, send happens below)
if(event->type == InputTypeLong) {
if(event->key == InputKeyUp) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_UP);
subghz_custom_btn_set_long(true);
} else if(event->key == InputKeyDown) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_DOWN);
subghz_custom_btn_set_long(true);
} else if(event->key == InputKeyLeft) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_LEFT);
subghz_custom_btn_set_long(true);
} else if(event->key == InputKeyRight) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_RIGHT);
subghz_custom_btn_set_long(true);
} else if(event->key == InputKeyDown) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_DOWN);
subghz_custom_btn_set_long(true);
} else if(event->key == InputKeyLeft) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_LEFT);
subghz_custom_btn_set_long(true);
} else if(event->key == InputKeyRight) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_RIGHT);
subghz_custom_btn_set_long(true);
}
}
}
if(event->key == InputKeyOk && event->type == InputTypePress) {
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK);
with_view_model(
subghz_transmitter->view,
SubGhzViewTransmitterModel * model,
{
furi_string_reset(model->temp_button_id);
model->draw_temp_button = false;
},
true);
subghz_transmitter->callback(
SubGhzCustomEventViewTransmitterSendStart, subghz_transmitter->context);
return true;
} else if(event->key == InputKeyOk && event->type == InputTypeRelease) {
subghz_transmitter->callback(
SubGhzCustomEventViewTransmitterSendStop, subghz_transmitter->context);
return true;
// OK button handling
if(event->key == InputKeyOk) {
if(event->type == InputTypePress) {
if(subghz_custom_btn_has_pages()) {
// Multi-page protocol: cycle pages, do NOT send
uint8_t max_pages = subghz_custom_btn_get_max_pages();
uint8_t next_page = (subghz_custom_btn_get_page() + 1) % max_pages;
subghz_custom_btn_set_page(next_page);
// Reset d-pad selection to OK so display shows original btn
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK);
with_view_model(
subghz_transmitter->view,
SubGhzViewTransmitterModel * model,
{
furi_string_reset(model->temp_button_id);
furi_string_printf(model->temp_button_id, "P%u", next_page + 1);
model->draw_temp_button = true;
},
true);
// Refresh display with new page mapping
subghz_transmitter->callback(
SubGhzCustomEventViewTransmitterPageChange, subghz_transmitter->context);
return true;
}
// Normal protocol: send original button
subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK);
with_view_model(
subghz_transmitter->view,
SubGhzViewTransmitterModel * model,
{
furi_string_reset(model->temp_button_id);
model->draw_temp_button = false;
},
true);
subghz_transmitter->callback(
SubGhzCustomEventViewTransmitterSendStart, subghz_transmitter->context);
return true;
} else if(event->type == InputTypeRelease) {
// Only stop TX if we actually started it (not a page toggle)
if(!subghz_custom_btn_has_pages()) {
subghz_transmitter->callback(
SubGhzCustomEventViewTransmitterSendStop, subghz_transmitter->context);
}
return true;
}
} // Finish "OK" key processing
if(subghz_custom_btn_is_allowed()) {

View File

@@ -0,0 +1,246 @@
---
Language: Cpp
AccessModifierOffset: -4
AlignAfterOpenBracket: BlockIndent
AlignArrayOfStructures: None
AlignConsecutiveAssignments:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
AlignFunctionPointers: false
PadOperators: true
AlignConsecutiveBitFields:
Enabled: true
AcrossEmptyLines: true
AcrossComments: true
AlignCompound: false
AlignFunctionPointers: false
PadOperators: true
AlignConsecutiveDeclarations:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
AlignFunctionPointers: false
PadOperators: true
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: false
AcrossComments: true
AlignCompound: true
AlignFunctionPointers: false
PadOperators: true
AlignConsecutiveShortCaseStatements:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCaseColons: false
AlignEscapedNewlines: Left
AlignOperands: Align
AlignTrailingComments:
Kind: Never
OverEmptyLines: 0
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowBreakBeforeNoexceptSpecifier: Never
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortCompoundRequirementOnASingleLine: true
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: WithoutElse
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: Yes
AttributeMacros:
- __capability
BinPackArguments: false
BinPackParameters: false
BitFieldColonSpacing: Both
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterExternBlock: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakAdjacentStringLiterals: true
BreakAfterAttributes: Leave
BreakAfterJavaFieldAnnotations: false
BreakArrays: true
BreakBeforeBinaryOperators: None
BreakBeforeConceptDeclarations: Always
BreakBeforeBraces: Attach
BreakBeforeInlineASMColon: OnlyMultiline
BreakBeforeTernaryOperators: false
BreakConstructorInitializers: AfterColon
BreakInheritanceList: AfterComma
BreakStringLiterals: false
ColumnLimit: 130
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerIndentWidth: 8
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
- M_EACH
IfMacros:
- KJ_IF_MAYBE
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '.*'
Priority: 1
SortPriority: 0
CaseSensitive: false
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
SortPriority: 0
CaseSensitive: false
- Regex: '.*'
Priority: 1
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: '(Test)?$'
IncludeIsMainSourceRegex: ''
IndentAccessModifiers: false
IndentCaseBlocks: false
IndentCaseLabels: false
IndentExternBlock: AfterExternBlock
IndentGotoLabels: true
IndentPPDirectives: None
IndentRequiresClause: false
IndentWidth: 4
IndentWrappedFunctionNames: true
InsertBraces: false
InsertNewlineAtEOF: true
InsertTrailingCommas: None
IntegerLiteralSeparator:
Binary: 0
BinaryMinDigits: 0
Decimal: 0
DecimalMinDigits: 0
Hex: 0
HexMinDigits: 0
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
KeepEmptyLinesAtEOF: false
LambdaBodyIndentation: Signature
LineEnding: DeriveLF
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 4
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PackConstructorInitializers: BinPack
PenaltyBreakAssignment: 10
PenaltyBreakBeforeFirstCallParameter: 30
PenaltyBreakComment: 10
PenaltyBreakFirstLessLess: 0
PenaltyBreakOpenParenthesis: 0
PenaltyBreakScopeResolution: 500
PenaltyBreakString: 10
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 100
PenaltyIndentedWhitespace: 0
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Left
PPIndentWidth: -1
QualifierAlignment: Leave
ReferenceAlignment: Pointer
ReflowComments: false
RemoveBracesLLVM: false
RemoveParentheses: Leave
RemoveSemicolon: true
RequiresClausePosition: OwnLine
RequiresExpressionIndentation: OuterScope
SeparateDefinitionBlocks: Leave
ShortNamespaceLines: 1
SkipMacroDefinitionBody: false
SortIncludes: Never
SortJavaStaticImport: Before
SortUsingDeclarations: Never
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceAroundPointerQualifiers: Default
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeJsonColon: false
SpaceBeforeParens: Never
SpaceBeforeParensOptions:
AfterControlStatements: false
AfterForeachMacros: false
AfterFunctionDefinitionName: false
AfterFunctionDeclarationName: false
AfterIfMacros: false
AfterOverloadedOperator: false
AfterPlacementOperator: true
AfterRequiresInClause: false
AfterRequiresInExpression: false
BeforeNonEmptyParentheses: false
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: Never
SpacesInContainerLiterals: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParens: Never
SpacesInParensOptions:
InCStyleCasts: false
InConditionalStatements: false
InEmptyParentheses: false
Other: false
SpacesInSquareBrackets: false
Standard: c++20
StatementAttributeLikeMacros:
- Q_EMIT
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 4
UseTab: Never
VerilogBreakBetweenInstancePorts: true
WhitespaceSensitiveMacros:
- STRINGIZE
- PP_STRINGIZE
- BOOST_PP_STRINGIZE
- NS_SWIFT_NAME
- CF_SWIFT_NAME
...

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Denr01
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,72 @@
# Chief Cooker
Your ultimate Flipper Zero restaurant pager tool. Be a _real chief_ of all the restaurants on the food court!
This app supports receiving, decoding, editing and sending restaurant pager signals.
**Developed & compatible with [Momentum firmware](https://github.com/Next-Flip/Momentum-Firmware).** Other firmwares are most likely not supported (but I've not tried).
## Video demo
[![Video 1](https://img.youtube.com/vi/iuQSyesS9-o/0.jpg)](https://youtube.com/shorts/iuQSyesS9-o)
More demos:
- [Video 2](https://youtube.com/shorts/KGDAGblbtFo)
- [Video 3](https://youtube.com/shorts/QqbfHF-yDiE)
## Disclaimer
I've built this app for research and learning purposes. But please, don't use it in a way that could hurt anyone or anything.
Use it responsibly, okay?
## [Usage instructions](instructions/instructions.md)
Please, read the [instructions](instructions/instructions.md) before using the app. It will definitely make your life easier!
## Features
- **Receive** signals from pager stations
- Automatically **decode** them and dsiplay station number, pager number and action (Ring/Mute/etc)
- Manually **change encoding in real-time** to look for the best one if automatically detected encoding is not working
- **Resend** captured message to specific pager to all at once to **make them all ring**!
- **Modify** captured signal, e.g. change pager number or action
- **Save** captured signals (and give each station a name)
- Create separate **categories** for each food court you are chief on
- Display signals from saved stations **by ther names** (instead of HEX code) or hide them from list
- **Send** signals from saved stations at any time, no need to capture it again
- Of course, suports working with **external CC1101 module** to cover the area of all the pagers on your food court!
## Supported protocols
- Princeton
- SMC5326
## Supported pager encodings
- Retekess TD157
- Retekess TD165/T119
- Retekess TD174
- L8R / Retekess T111 (not tested)
- L8S / iBells ZJ-68 (check [source code](app/pager/decoder/L8SDecoder.hpp#L8) for description)
## Contributing
If you want to add any new pager encoding, please feel free to create PR with it!
Also you can open issue and share with me any captured data (and at least pager number) if you have any and maybe I'll try create a decoder for it
## Building
If you build the source code just with regular `ufbt` command, the app will probably crash due to out of memory error because your device will have less that 10 kb of free RAM on the "Scan" screen.
This is because after you compile the app with ufbt, the result executable will contain hundreds of sections with very long names like `.fast.rel.text._ZNSt17_Function_handlerIFvmEZN18PagerActionsScreenC4EP9AppConfigSt8functionIFP15StoredPagerDatavEEP12PagerDecoderP13PagerProtocolP12SubGhzModuleEUlmE_E9_M_invokeERKSt9_Any_dataOm`.
**These names stay in RAM during the execution and consume about 20kb of heap!**
The reason for it is [name mangling](https://en.wikipedia.org/wiki/Name_mangling). Perhaps, the gcc parameter `-fno-mangle` could disable it, but unfortunately, it is not possible to pass any arguments to gcc when you compile app with `ufbt`.
Luckily the sections inside the compiled file can be renamed using gcc's `objcopy` tool with `--rename-section` parameter. To automate it, I built a small python script which renames them all and gives them short names like `_s1`, `_s2`, `_s228` etc...
**Therfore you must use [scripts/build-and-clear.py](scripts/build-and-clear.py) script instead!** It will build, rename sections and upload the fap to flipper.
After building and cleaning your `.fap` with it, you'll get extra +20kb of free RAM which will make compiled app work stably.
The `.fap` files under the release tab are already cleared with this script, so if you just want to use this app without modifications, just forget about it and download the latest one from releases.
## Support & Donate
> [PayPal](https://paypal.me/denr01)
## Special Thanks
- [meoker/pagger](https://github.com/meoker/pagger) for Retekess pager encodings
- This [awesome repository](https://dev.xcjs.com/r0073dl053r/flipper-playground/-/tree/main/Sub-GHz/Restaurant_Pagers?ref_type=heads) for Retekess T111 and iBells ZJ-68 files

View File

@@ -0,0 +1,33 @@
#pragma once
#include "lib/ui/UiManager.hpp"
#include "lib/hardware/notification/Notification.hpp"
#include "lib/hardware/subghz/FrequencyManager.hpp"
#include "AppConfig.hpp"
#include "app/screen/MainMenuScreen.hpp"
using namespace std;
class App {
public:
void Run() {
UiManager* ui = UiManager::GetInstance();
ui->InitGui();
FrequencyManager* frequencyManager = FrequencyManager::GetInstance();
AppConfig* config = new AppConfig();
config->Load();
MainMenuScreen* mainMenuScreen = new MainMenuScreen(config);
ui->PushView(mainMenuScreen->GetView());
ui->RunEventLoop();
delete frequencyManager;
delete mainMenuScreen;
delete config;
delete ui;
Notification::Dispose();
}
};

View File

@@ -0,0 +1,78 @@
#pragma once
#include <cstdint>
#include "app/AppFileSystem.hpp"
#include "lib/file/FileManager.hpp"
#include "app/pager/SavedStationStrategy.hpp"
#define KEY_CONFIG_FREQUENCY "Frequency"
#define KEY_CONFIG_MAX_PAGERS "MaxPagerForBatchOrDetection"
#define KEY_CONFIG_REPEATS "SignalRepeats"
#define KEY_CONFIG_SAVED_STRATEGY "SavedStationStrategy"
#define KEY_CONFIG_AUTOSAVE "AutosaveFoundSignals"
#define KEY_CONFIG_USER_CATGEGORY "UserCategory"
class AppConfig {
public:
uint32_t Frequency = 433920000;
uint32_t MaxPagerForBatchOrDetection = 30;
uint32_t SignalRepeats = 10;
SavedStationStrategy SavedStrategy = SHOW_NAME;
bool AutosaveFoundSignals = true;
String* CurrentUserCategory = NULL;
private:
void readFromFile(FlipperFile* file) {
String* userCat = new String();
uint32_t savedStrategyValue = SavedStrategy;
if(CurrentUserCategory != NULL) {
delete CurrentUserCategory;
}
file->ReadUInt32(KEY_CONFIG_FREQUENCY, &Frequency);
file->ReadUInt32(KEY_CONFIG_MAX_PAGERS, &MaxPagerForBatchOrDetection);
file->ReadUInt32(KEY_CONFIG_REPEATS, &SignalRepeats);
file->ReadUInt32(KEY_CONFIG_SAVED_STRATEGY, &savedStrategyValue);
file->ReadBool(KEY_CONFIG_AUTOSAVE, &AutosaveFoundSignals);
file->ReadString(KEY_CONFIG_USER_CATGEGORY, userCat);
SavedStrategy = static_cast<enum SavedStationStrategy>(savedStrategyValue);
if(!userCat->isEmpty()) {
CurrentUserCategory = userCat;
} else {
CurrentUserCategory = NULL;
delete userCat;
}
}
void writeToFile(FlipperFile* file) {
file->WriteUInt32(KEY_CONFIG_FREQUENCY, Frequency);
file->WriteUInt32(KEY_CONFIG_MAX_PAGERS, MaxPagerForBatchOrDetection);
file->WriteUInt32(KEY_CONFIG_REPEATS, SignalRepeats);
file->WriteUInt32(KEY_CONFIG_SAVED_STRATEGY, SavedStrategy);
file->WriteBool(KEY_CONFIG_AUTOSAVE, AutosaveFoundSignals);
file->WriteString(KEY_CONFIG_USER_CATGEGORY, CurrentUserCategory != NULL ? CurrentUserCategory->cstr() : "");
}
public:
void Load() {
FlipperFile* configFile = FileManager().OpenRead(CONFIG_FILE_PATH);
if(configFile != NULL) {
readFromFile(configFile);
delete configFile;
}
}
void Save() {
FlipperFile* configFile = FileManager().OpenWrite(CONFIG_FILE_PATH);
if(configFile != NULL) {
writeToFile(configFile);
delete configFile;
}
}
const char* GetCurrentUserCategoryCstr() {
return CurrentUserCategory == NULL ? NULL : CurrentUserCategory->cstr();
}
};

View File

@@ -0,0 +1,188 @@
#pragma once
#include <storage/storage.h>
#include <forward_list>
#include "app/pager/PagerSerializer.hpp"
#include "lib/file/FileManager.hpp"
#include "pager/data/NamedPagerData.hpp"
// .fff stands for (f)lipper (f)ile (f)ormat
#define CONFIG_FILE_PATH APP_DATA_PATH("config.fff")
#define STATIONS_PATH APP_DATA_PATH("stations")
#define STATIONS_PATH_OF(path) STATIONS_PATH "/" path
#define SAVED_STATIONS_PATH STATIONS_PATH_OF("saved")
#define AUTOSAVED_STATIONS_PATH STATIONS_PATH_OF("autosaved")
#define MAX_FILENAME_LENGTH 16
using namespace std;
enum CategoryType {
User,
Autosaved,
NotSelected,
};
class AppFileSysytem {
private:
String* getCategoryPath(CategoryType categoryType, const char* category) {
switch(categoryType) {
case User:
if(category != NULL) {
return new String("%s/%s", SAVED_STATIONS_PATH, category);
} else {
return new String(SAVED_STATIONS_PATH);
}
case Autosaved:
return new String("%s/%s", AUTOSAVED_STATIONS_PATH, category);
default:
case NotSelected:
return NULL;
}
}
String* getFilePath(CategoryType categoryType, const char* category, StoredPagerData* pager) {
String* categoryPath = getCategoryPath(categoryType, category);
String* pagerFilename = PagerSerializer().GetFilename(pager);
String* filePath = new String("%s/%s", categoryPath->cstr(), pagerFilename->cstr());
delete categoryPath;
delete pagerFilename;
return filePath;
}
public:
int GetCategories(forward_list<char*>* categoryList, CategoryType categoryType) {
const char* dirPath;
switch(categoryType) {
case User:
dirPath = SAVED_STATIONS_PATH;
break;
case Autosaved:
dirPath = AUTOSAVED_STATIONS_PATH;
break;
default:
return 0;
}
FileManager fileManager = FileManager();
Directory* dir = fileManager.OpenDirectory(dirPath);
uint16_t categoriesLoaded = 0;
if(dir != NULL) {
char fileName[MAX_FILENAME_LENGTH];
while(dir->GetNextDir(fileName, MAX_FILENAME_LENGTH)) {
char* category = new char[strlen(fileName)];
strcpy(category, fileName);
categoryList->push_front(category);
categoriesLoaded++;
}
}
delete dir;
return categoriesLoaded;
}
size_t GetStationsFromDirectory(
forward_list<NamedPagerData>* stationList,
ProtocolAndDecoderProvider* pdProvider,
CategoryType categoryType,
const char* category,
bool loadNames
) {
FileManager fileManager = FileManager();
String* stationDirPath = getCategoryPath(categoryType, category);
Directory* dir = fileManager.OpenDirectory(stationDirPath->cstr());
PagerSerializer serializer = PagerSerializer();
size_t stationsLoaded = 0;
if(dir != NULL) {
char fileName[MAX_FILENAME_LENGTH];
while(dir->GetNextFile(fileName, MAX_FILENAME_LENGTH)) {
String* stationName = new String();
StoredPagerData pager =
serializer.LoadPagerData(&fileManager, stationName, stationDirPath->cstr(), fileName, pdProvider);
if(!loadNames) {
delete stationName;
stationName = NULL;
}
NamedPagerData returnData = NamedPagerData();
returnData.storedData = pager;
returnData.name = stationName;
stationList->push_front(returnData);
stationsLoaded++;
}
}
delete dir;
delete stationDirPath;
return stationsLoaded;
}
String* GetOnlyStationName(CategoryType categoryType, const char* category, StoredPagerData* pager) {
FileManager fileManager = FileManager();
String* categoryPath = getCategoryPath(categoryType, category);
String* name = PagerSerializer().LoadOnlyStationName(&fileManager, categoryPath->cstr(), pager);
delete categoryPath;
return name;
}
void AutoSave(StoredPagerData* storedData, PagerDecoder* decoder, PagerProtocol* protocol, uint32_t frequency) {
DateTime datetime;
furi_hal_rtc_get_datetime(&datetime);
String* todayDate = new String("%d-%02d-%02d", datetime.year, datetime.month, datetime.day);
String* todaysDir = getCategoryPath(Autosaved, todayDate->cstr());
FileManager fileManager = FileManager();
fileManager.CreateDirIfNotExists(STATIONS_PATH);
fileManager.CreateDirIfNotExists(AUTOSAVED_STATIONS_PATH);
fileManager.CreateDirIfNotExists(todaysDir->cstr());
PagerSerializer().SavePagerData(&fileManager, todaysDir->cstr(), "", storedData, decoder, protocol, frequency);
delete todaysDir;
delete todayDate;
}
void SaveToUserCategory(
const char* userCategory,
const char* stationName,
StoredPagerData* storedData,
PagerDecoder* decoder,
PagerProtocol* protocol,
uint32_t frequency
) {
String* catDir = getCategoryPath(User, userCategory);
FileManager fileManager = FileManager();
fileManager.CreateDirIfNotExists(STATIONS_PATH);
fileManager.CreateDirIfNotExists(AUTOSAVED_STATIONS_PATH);
fileManager.CreateDirIfNotExists(catDir->cstr());
PagerSerializer().SavePagerData(&fileManager, catDir->cstr(), stationName, storedData, decoder, protocol, frequency);
delete catDir;
}
void DeletePager(const char* userCategory, StoredPagerData* storedData) {
String* filePath = getFilePath(User, userCategory, storedData);
FileManager().DeleteFile(filePath->cstr());
delete filePath;
}
void DeleteCategory(const char* userCategory) {
String* catPath = getCategoryPath(User, userCategory);
FileManager().DeleteFile(catPath->cstr());
delete catPath;
}
};

View File

@@ -0,0 +1,16 @@
#pragma once
#include "lib/hardware/notification/Notification.hpp"
const NotificationSequence NOTIFICATION_PAGER_RECEIVE = {
&message_vibro_on,
&message_note_e6,
&message_blue_255,
&message_delay_50,
&message_sound_off,
&message_vibro_off,
NULL,
};

View File

@@ -0,0 +1,42 @@
#pragma once
enum PagerAction {
UNKNOWN,
RING,
MUTE,
DESYNC,
TURN_OFF_ALL,
PagerActionCount,
};
class PagerActions {
public:
static const char* GetDescription(PagerAction action) {
switch(action) {
case UNKNOWN:
return "?";
case RING:
return "RING";
case MUTE:
return "MUTE";
case DESYNC:
return "DESYNC_ALL";
case TURN_OFF_ALL:
return "ALL_OFF";
default:
return "";
}
}
static bool IsPagerActionSpecial(PagerAction action) {
switch(action) {
case DESYNC:
case TURN_OFF_ALL:
return true;
default:
return false;
}
}
};

View File

@@ -0,0 +1,319 @@
#pragma once
#include "ProtocolAndDecoderProvider.hpp"
#include <cstring>
#include <forward_list>
#include "lib/hardware/subghz/FrequencyManager.hpp"
#include "lib/hardware/subghz/data/SubGhzReceivedData.hpp"
#include "app/AppConfig.hpp"
#include "data/ReceivedPagerData.hpp"
#include "data/KnownStationData.hpp"
#include "protocol/PrincetonProtocol.hpp"
#include "protocol/Smc5326Protocol.hpp"
#include "decoder/Td157Decoder.hpp"
#include "decoder/Td165Decoder.hpp"
#include "decoder/Td174Decoder.hpp"
#include "decoder/L8RDecoder.hpp"
#include "decoder/L8SDecoder.hpp"
#undef LOG_TAG
#define LOG_TAG "PGR_RCV"
#define MAX_REPEATS 99
#define PAGERS_ARRAY_SIZE_MULTIPLIER 8
using namespace std;
class PagerReceiver : public ProtocolAndDecoderProvider {
public:
static const uint8_t protocolsCount = 2;
PagerProtocol* protocols[protocolsCount]{
new PrincetonProtocol(),
new Smc5326Protocol(),
};
static const uint8_t decodersCount = 5;
PagerDecoder* decoders[decodersCount]{
new Td157Decoder(),
new Td165Decoder(),
new Td174Decoder(),
new L8RDecoder(),
new L8SDecoder(),
};
private:
AppConfig* config;
uint16_t nextPagerIndex = 0;
uint16_t pagersArraySize = PAGERS_ARRAY_SIZE_MULTIPLIER;
StoredPagerData* pagers = new StoredPagerData[pagersArraySize];
size_t knownStationsSize = 0;
KnownStationData* knownStations;
uint32_t lastFrequency = 0;
uint8_t lastFrequencyIndex = 0;
bool knownStationsLoaded = false;
const char* userCategory;
void loadKnownStations() {
AppFileSysytem appFilesystem;
forward_list<NamedPagerData> stations;
bool withNames = config->SavedStrategy == SHOW_NAME;
size_t count = appFilesystem.GetStationsFromDirectory(&stations, this, User, userCategory, withNames);
knownStations = new KnownStationData[count];
for(size_t i = 0; i < count; i++) {
knownStations[i] = buildKnownStationWithName(stations.front());
stations.pop_front();
}
knownStationsSize = count;
knownStationsLoaded = true;
}
void unloadKnownStations() {
for(size_t i = 0; i < knownStationsSize; i++) {
if(knownStations[i].name != NULL) {
delete knownStations[i].name;
}
}
delete[] knownStations;
knownStationsLoaded = false;
knownStationsSize = 0;
}
KnownStationData buildKnownStationWithName(NamedPagerData pager) {
KnownStationData data = KnownStationData();
data.frequency = pager.storedData.frequency;
data.protocol = pager.storedData.protocol;
data.decoder = pager.storedData.decoder;
data.station = decoders[pager.storedData.decoder]->GetStation(pager.storedData.data);
data.name = pager.name;
return data;
}
KnownStationData buildKnownStationWithoutName(StoredPagerData* pager) {
KnownStationData data = KnownStationData();
data.frequency = pager->frequency;
data.protocol = pager->protocol;
data.decoder = pager->decoder;
data.station = decoders[pager->decoder]->GetStation(pager->data);
data.name = NULL;
return data;
}
PagerDecoder* getDecoder(StoredPagerData* pagerData) {
for(size_t i = 0; i < decodersCount; i++) {
pagerData->decoder = i;
if(IsKnown(pagerData)) {
return decoders[i];
}
}
for(size_t i = 0; i < decodersCount; i++) {
if(decoders[i]->GetPager(pagerData->data) <= config->MaxPagerForBatchOrDetection) {
return decoders[i];
}
}
return decoders[0];
}
void addPager(StoredPagerData data) {
if(nextPagerIndex == pagersArraySize) {
pagersArraySize += PAGERS_ARRAY_SIZE_MULTIPLIER;
StoredPagerData* newPagers = new StoredPagerData[pagersArraySize];
for(int i = 0; i < nextPagerIndex; i++) {
newPagers[i] = pagers[i];
}
delete[] pagers;
pagers = newPagers;
}
pagers[nextPagerIndex++] = data;
}
public:
PagerReceiver(AppConfig* config) {
this->config = config;
for(size_t i = 0; i < protocolsCount; i++) {
protocols[i]->id = i;
}
for(size_t i = 0; i < decodersCount; i++) {
decoders[i]->id = i;
}
SetUserCategory(config->CurrentUserCategory);
}
void SetUserCategory(String* category) {
SetUserCategory(category != NULL ? category->cstr() : NULL);
}
const char* GetCurrentUserCategory() {
return userCategory;
}
void SetUserCategory(const char* category) {
userCategory = category;
}
PagerProtocol* GetProtocolByName(const char* systemProtocolName) {
for(size_t i = 0; i < protocolsCount; i++) {
if(strcmp(systemProtocolName, protocols[i]->GetSystemName()) == 0) {
return protocols[i];
}
}
return NULL;
}
PagerDecoder* GetDecoderByName(const char* shortName) {
for(size_t i = 0; i < decodersCount; i++) {
if(strcmp(shortName, decoders[i]->GetShortName()) == 0) {
return decoders[i];
}
}
return NULL;
}
void ReloadKnownStations() {
unloadKnownStations();
loadKnownStations();
}
void LoadStationsFromDirectory(
CategoryType categoryType,
const char* category,
function<void(ReceivedPagerData*)> pagerHandler
) {
AppFileSysytem appFilesystem;
forward_list<NamedPagerData> stations;
bool withNames = !knownStationsLoaded && config->SavedStrategy == SHOW_NAME;
int count = appFilesystem.GetStationsFromDirectory(&stations, this, categoryType, category, withNames);
delete[] pagers;
pagers = new StoredPagerData[count];
if(!knownStationsLoaded) {
knownStations = new KnownStationData[count];
}
for(int i = 0; i < count; i++) {
NamedPagerData pagerData = stations.front();
pagers[i] = pagerData.storedData;
if(!knownStationsLoaded) {
knownStations[i] = buildKnownStationWithName(pagerData);
}
stations.pop_front();
pagerHandler(new ReceivedPagerData(PagerGetter(i), i, true));
}
if(!knownStationsLoaded) {
knownStationsSize = count;
}
nextPagerIndex = count;
pagersArraySize = count;
knownStationsLoaded = true;
}
PagerDataGetter PagerGetter(size_t index) {
return [this, index]() { return &pagers[index]; };
}
String* GetName(StoredPagerData* pager) {
uint32_t stationId = buildKnownStationWithoutName(pager).toInt();
for(size_t i = 0; i < knownStationsSize; i++) {
if(knownStations[i].toInt() == stationId) {
return knownStations[i].name;
}
}
return NULL;
}
bool IsKnown(StoredPagerData* pager) {
uint32_t stationId = buildKnownStationWithoutName(pager).toInt();
for(size_t i = 0; i < knownStationsSize; i++) {
if(knownStations[i].toInt() == stationId) {
return true;
}
}
return false;
}
ReceivedPagerData* Receive(SubGhzReceivedData* data) {
PagerProtocol* protocol = GetProtocolByName(data->GetProtocolName());
if(protocol == NULL) {
return NULL;
}
int index = -1;
uint32_t dataHash = data->GetHash();
for(size_t i = 0; i < nextPagerIndex; i++) {
if(pagers[i].data == dataHash && pagers[i].protocol == protocol->id) {
if(pagers[i].repeats < MAX_REPEATS) {
pagers[i].repeats++;
} else {
return NULL; // no need to modify element any more
}
index = i;
break;
}
}
bool isNew = index < 0;
if(isNew) {
if(data->GetFrequency() != lastFrequency) {
lastFrequencyIndex = FrequencyManager::GetInstance()->GetFrequencyIndex(data->GetFrequency());
lastFrequency = data->GetFrequency();
}
StoredPagerData storedData = StoredPagerData();
storedData.data = dataHash;
storedData.protocol = protocol->id;
storedData.repeats = 1;
storedData.te = data->GetTE();
storedData.frequency = lastFrequencyIndex;
storedData.decoder = getDecoder(&storedData)->id;
storedData.edited = false;
if(config->SavedStrategy == HIDE && IsKnown(&storedData)) {
return NULL;
}
if(config->AutosaveFoundSignals) {
AppFileSysytem().AutoSave(&storedData, decoders[storedData.decoder], protocol, lastFrequency);
}
index = nextPagerIndex;
addPager(storedData);
}
return new ReceivedPagerData(PagerGetter(index), index, isNew);
}
~PagerReceiver() {
for(PagerProtocol* protocol : protocols) {
delete protocol;
}
for(PagerDecoder* decoder : decoders) {
delete decoder;
}
delete[] pagers;
unloadKnownStations();
}
};

View File

@@ -0,0 +1,100 @@
#pragma once
#include "ProtocolAndDecoderProvider.hpp"
#include "lib/String.hpp"
#include "lib/file/FileManager.hpp"
#include "lib/file/FlipperFile.hpp"
#include "data/StoredPagerData.hpp"
#include "lib/hardware/subghz/FrequencyManager.hpp"
#include "protocol/PagerProtocol.hpp"
#include "decoder/PagerDecoder.hpp"
#define KEY_PAGER_STATION_NAME "StationName"
#define KEY_PAGER_FREQUENCY "Frequency"
#define KEY_PAGER_PROTOCOL "Protocol"
#define KEY_PAGER_DECODER "Decoder"
#define KEY_PAGER_DATA "Data"
#define KEY_PAGER_TE "TE"
#define NAME_MIN_LENGTH 2
#define NAME_MAX_LENGTH 20
class PagerSerializer {
private:
public:
String* GetFilename(StoredPagerData* pager) {
return new String("%06X.fff", pager->data);
}
void SavePagerData(
FileManager* fileManager,
const char* dir,
const char* stationName,
StoredPagerData* pager,
PagerDecoder* decoder,
PagerProtocol* protocol,
uint32_t frequency
) {
String* fileName = GetFilename(pager);
FlipperFile* stationFile = fileManager->OpenWrite(dir, fileName->cstr());
stationFile->WriteString(KEY_PAGER_STATION_NAME, stationName);
stationFile->WriteUInt32(KEY_PAGER_FREQUENCY, frequency);
stationFile->WriteString(KEY_PAGER_PROTOCOL, protocol->GetSystemName());
stationFile->WriteString(KEY_PAGER_DECODER, decoder->GetShortName());
stationFile->WriteUInt32(KEY_PAGER_TE, pager->te);
stationFile->WriteHex(KEY_PAGER_DATA, pager->data);
delete stationFile;
delete fileName;
}
String* LoadOnlyStationName(FileManager* fileManager, const char* dir, StoredPagerData* pager) {
String* filename = GetFilename(pager);
FlipperFile* stationFile = fileManager->OpenRead(dir, filename->cstr());
delete filename;
String* stationName = NULL;
if(stationFile != NULL) {
stationName = new String();
stationFile->ReadString(KEY_PAGER_STATION_NAME, stationName);
delete stationFile;
}
return stationName;
}
StoredPagerData LoadPagerData(
FileManager* fileManager,
String* stationName,
const char* dir,
const char* fileName,
ProtocolAndDecoderProvider* pdProvider
) {
FlipperFile* stationFile = fileManager->OpenRead(dir, fileName);
uint32_t te = 0;
uint64_t hex = 0;
uint32_t frequency = 0;
String protocolName;
String decoderName;
stationFile->ReadString(KEY_PAGER_STATION_NAME, stationName);
stationFile->ReadUInt32(KEY_PAGER_FREQUENCY, &frequency);
stationFile->ReadString(KEY_PAGER_PROTOCOL, &protocolName);
stationFile->ReadString(KEY_PAGER_DECODER, &decoderName);
stationFile->ReadUInt32(KEY_PAGER_TE, &te);
stationFile->ReadHex(KEY_PAGER_DATA, &hex);
delete stationFile;
StoredPagerData pager;
pager.data = hex;
pager.te = te;
pager.edited = false;
pager.frequency = FrequencyManager::GetInstance()->GetFrequencyIndex(frequency);
pager.protocol = pdProvider->GetProtocolByName(protocolName.cstr())->id;
pager.decoder = pdProvider->GetDecoderByName(decoderName.cstr())->id;
return pager;
}
};

View File

@@ -0,0 +1,12 @@
#pragma once
#include "protocol/PagerProtocol.hpp"
#include "decoder/PagerDecoder.hpp"
class ProtocolAndDecoderProvider {
public:
virtual PagerProtocol* GetProtocolByName(const char* name) = 0;
virtual PagerDecoder* GetDecoderByName(const char* name) = 0;
virtual ~ProtocolAndDecoderProvider() {
}
};

View File

@@ -0,0 +1,9 @@
#pragma once
enum SavedStationStrategy {
IGNORE, // don't check if station is saved, show as unknown
SHOW_NAME, // show station name instead of hex and station number
HIDE, // hide all station signals from the search
SavedStationStrategyValuesCount,
};

View File

@@ -0,0 +1,28 @@
#pragma once
#include "lib/String.hpp"
#include <cstdint>
struct KnownStationData {
uint8_t frequency : 8;
uint8_t protocol : 2;
uint8_t decoder : 4;
uint8_t unused : 2; // align
uint16_t station : 16;
String* name;
public:
uint32_t toInt();
};
union KnownStationDataUnion {
KnownStationData stationData;
uint32_t intValue;
};
uint32_t KnownStationData::toInt() {
KnownStationDataUnion u;
u.stationData = *this;
u.stationData.unused = 0;
return u.intValue;
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include "StoredPagerData.hpp"
#include "lib/String.hpp"
struct NamedPagerData {
StoredPagerData storedData;
String* name;
};

View File

@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include "StoredPagerData.hpp"
class ReceivedPagerData {
private:
PagerDataGetter getStoredData;
uint32_t index;
bool isNew;
public:
ReceivedPagerData(PagerDataGetter storedDataGetter, uint32_t index, bool isNew) {
this->getStoredData = storedDataGetter;
this->index = index;
this->isNew = isNew;
}
bool IsNew() {
return isNew;
}
uint32_t GetIndex() {
return index;
}
StoredPagerData* GetData() {
return getStoredData();
}
};

View File

@@ -0,0 +1,33 @@
#pragma once
#include <cstdint>
#include <functional>
using namespace std;
struct StoredPagerData {
// first 4-byte
uint32_t data : 25;
uint8_t repeats : 7;
// second 4-byte
// byte 1
uint8_t frequency : 8;
// byte 2
uint8_t decoder : 4; // max 16 decoders, enough for now
uint8_t protocol : 2; // max 4 protocols (only )
bool edited : 1;
uint8_t : 0;
// byte 3-4
uint16_t te : 11; // 2048 values should be enough
// 5 bits still unused
};
// StoredPagerData is short-living because it's stored as array of stack allocated objects in PagerReceiver class.
// If array size changes, it reallocates all the objects on the new addresses in memory. We could store pointers in array instead of stack objects,
// but it would take more memory which is not acceptable (sizeof(StoredPagerData) + sizeof(StoredPagerData*)) vs sizeof(StoredPagerData).
// That's why we pass the getter instead of object itself, to make sure we always have the right pointer to the StoredPagerData structure.
typedef function<StoredPagerData*()> PagerDataGetter;

View File

@@ -0,0 +1,81 @@
#pragma once
#include "PagerDecoder.hpp"
#define T111_ACTION_RING 0
// Retekess T111 / L8R
// L8R — (L)ast (8) bits (R)eversed order (for pager number)
// seems to be Retekess T111 encoding, but cannot check it due to lack of information
// So I decided to keep it's name as L8R
class L8RDecoder : public PagerDecoder {
private:
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station (any maybe more)
const uint32_t actionMask = 0b11100000000; // next 3 bits are action (possibly, just my guess, may be they are also station)
const uint32_t pagerMask = 0b11111111; // and the last 8 bits seem to be pager number
const uint8_t stationBitCount = 13;
const uint8_t stationOffset = 11;
const uint8_t actionBitCount = 3;
const uint8_t actionOffset = 8;
const uint8_t pagerBitCount = 8;
public:
const char* GetShortName() {
return "L8R";
}
uint16_t GetStation(uint32_t data) {
uint32_t stationReversed = (data & stationMask) >> stationOffset;
return reverseBits(stationReversed, stationBitCount);
}
uint16_t GetPager(uint32_t data) {
uint32_t pagerReversed = data & pagerMask;
return reverseBits(pagerReversed, pagerBitCount);
}
uint8_t GetActionValue(uint32_t data) {
uint32_t actionReversed = (data & actionMask) >> actionOffset;
return reverseBits(actionReversed, actionBitCount);
}
PagerAction GetAction(uint32_t data) {
switch(GetActionValue(data)) {
case T111_ACTION_RING:
return RING;
default:
return UNKNOWN;
}
}
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
return (data & ~pagerMask) | reverseBits(pagerNum, pagerBitCount);
}
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
uint32_t actionCleared = data & ~actionMask;
return actionCleared | (reverseBits(actionValue, actionBitCount) << actionOffset);
}
uint32_t SetAction(uint32_t data, PagerAction action) {
switch(action) {
case RING:
return SetActionValue(data, T111_ACTION_RING);
default:
return data;
}
}
bool IsSupported(PagerAction) {
return false;
}
uint8_t GetActionsCount() {
return 8;
}
};

View File

@@ -0,0 +1,61 @@
#pragma once
#include "core/core_defines.h"
#include "PagerDecoder.hpp"
// iBells ZJ-68 / L8S
// L8S — (L)ast (8) bits (S)traight order (non-reversed) (for pager number)
class L8SDecoder : public PagerDecoder {
private:
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station (let it be)
const uint32_t actionMask = 0b11100000000; // next 3 bits are action (possibly, just my guess, may be they are also station)
const uint32_t pagerMask = 0b11111111; // and the last 8 bits should be enough for pager number
const uint8_t stationOffset = 11;
const uint8_t actionOffset = 8;
public:
const char* GetShortName() {
return "L8S";
}
uint16_t GetStation(uint32_t data) {
return (data & stationMask) >> stationOffset;
}
uint16_t GetPager(uint32_t data) {
return data & pagerMask;
}
uint8_t GetActionValue(uint32_t data) {
return (data & actionMask) >> actionOffset;
}
PagerAction GetAction(uint32_t data) {
UNUSED(data);
return UNKNOWN;
}
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
return (data & ~pagerMask) | pagerNum;
}
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
uint32_t actionCleared = data & ~actionMask;
return actionCleared | (actionValue << actionOffset);
}
uint32_t SetAction(uint32_t data, PagerAction action) {
UNUSED(action);
return data;
}
bool IsSupported(PagerAction) {
return false;
}
uint8_t GetActionsCount() {
return 8;
}
};

View File

@@ -0,0 +1,52 @@
#pragma once
#include <cstdint>
#include "../PagerAction.hpp"
using namespace std;
class PagerDecoder {
public:
uint8_t id;
virtual const char* GetShortName() = 0;
virtual uint16_t GetStation(uint32_t data) = 0;
virtual uint16_t GetPager(uint32_t data) = 0;
virtual uint32_t SetPager(uint32_t data, uint16_t pagerNum) = 0;
virtual uint8_t GetActionValue(uint32_t data) = 0;
virtual PagerAction GetAction(uint32_t data) = 0;
virtual uint32_t SetAction(uint32_t data, PagerAction action) = 0;
virtual uint32_t SetActionValue(uint32_t data, uint8_t action) = 0;
virtual bool IsSupported(PagerAction action) = 0;
virtual uint8_t GetActionsCount() = 0;
uint8_t GetSupportedActionsCount() {
uint8_t count = 0;
for(uint8_t i = 0; i < PagerActionCount; i++) {
if(IsSupported(static_cast<enum PagerAction>(i))) {
count++;
}
}
return count;
}
virtual ~PagerDecoder() {
}
protected:
uint32_t reverseBits(uint32_t number, int count) {
uint32_t rev = 0;
while(count-- > 0) {
rev <<= 1;
if((number & 1) == 1) {
rev ^= 1;
}
number >>= 1;
}
return rev;
}
};

View File

@@ -0,0 +1,87 @@
#pragma once
#include "PagerDecoder.hpp"
#define TD157_ACTION_RING 0b0010
#define TD157_ACTION_TURN_OFF_ALL 0b1111
#define TD157_PAGER_TURN_OFF_ALL 999
// Retekess TD157
class Td157Decoder : public PagerDecoder {
private:
const uint32_t stationMask = 0b111111111100000000000000; // leading 10 bits (of 24) are station
const uint32_t pagerMask = 0b11111111110000; // next 10 bits are pager
const uint32_t actionMask = 0b1111; // and the last 4 bits is action
const uint8_t stationOffset = 14;
const uint8_t pagerOffset = 4;
public:
const char* GetShortName() {
return "TD157";
}
uint16_t GetStation(uint32_t data) {
uint32_t station = (data & stationMask) >> stationOffset;
return (uint16_t)station;
}
uint16_t GetPager(uint32_t data) {
uint32_t pager = (data & pagerMask) >> pagerOffset;
return (uint16_t)pager;
}
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
uint32_t pagerClearedData = data & ~pagerMask;
return pagerClearedData | (pagerNum << pagerOffset);
}
uint8_t GetActionValue(uint32_t data) {
return data & actionMask;
}
PagerAction GetAction(uint32_t data) {
switch(GetActionValue(data)) {
case TD157_ACTION_RING:
return RING;
case TD157_ACTION_TURN_OFF_ALL:
if(GetPager(data) == TD157_PAGER_TURN_OFF_ALL) {
return TURN_OFF_ALL;
}
return UNKNOWN;
default:
return UNKNOWN;
}
}
uint32_t SetAction(uint32_t data, PagerAction action) {
switch(action) {
case RING:
return SetActionValue(data, TD157_ACTION_RING);
case TURN_OFF_ALL:
return SetActionValue(SetPager(data, TD157_PAGER_TURN_OFF_ALL), TD157_ACTION_TURN_OFF_ALL);
default:
return data;
}
}
virtual uint32_t SetActionValue(uint32_t data, uint8_t action) {
return (data & ~actionMask) | action;
}
bool IsSupported(PagerAction action) {
switch(action) {
case RING:
case TURN_OFF_ALL:
return true;
default:
return false;
}
return false;
}
uint8_t GetActionsCount() {
return actionMask + 1;
}
};

View File

@@ -0,0 +1,98 @@
#pragma once
#include "PagerDecoder.hpp"
#define TD165_ACTION_RING 0
#define TD165_ACTION_MUTE 1
#define TD165_PAGER_TURN_OFF_ALL 1005
// Retekess TD165/T119
class Td165Decoder : public PagerDecoder {
private:
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station
const uint32_t pagerMask = 0b11111111110; // next 10 bits are pager
const uint32_t actionMask = 0b1; // and the last 1 bit is action
const uint8_t stationBitCount = 13;
const uint8_t stationOffset = 11;
const uint8_t pagerBitCount = 10;
const uint8_t pagerOffset = 1;
public:
const char* GetShortName() {
return "TD165";
}
uint16_t GetStation(uint32_t data) {
uint32_t stationReversed = (data & stationMask) >> stationOffset;
return reverseBits(stationReversed, stationBitCount);
}
uint16_t GetPager(uint32_t data) {
uint32_t pagerReversed = (data & pagerMask) >> pagerOffset;
return reverseBits(pagerReversed, pagerBitCount);
}
uint8_t GetActionValue(uint32_t data) {
return data & actionMask;
}
PagerAction GetAction(uint32_t data) {
switch(GetActionValue(data)) {
case TD165_ACTION_RING:
if(GetPager(data) == TD165_PAGER_TURN_OFF_ALL) {
return TURN_OFF_ALL;
}
return RING;
case TD165_ACTION_MUTE:
return MUTE;
default:
return UNKNOWN;
}
}
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
uint32_t pagerCleared = data & ~pagerMask;
return pagerCleared | (reverseBits(pagerNum, pagerBitCount) << pagerOffset);
}
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
return (data & ~actionMask) | actionValue;
}
uint32_t SetAction(uint32_t data, PagerAction action) {
switch(action) {
case RING:
return SetActionValue(data, TD165_ACTION_RING);
case MUTE:
return SetActionValue(data, TD165_ACTION_MUTE);
case TURN_OFF_ALL:
return SetActionValue(SetPager(data, TD165_PAGER_TURN_OFF_ALL), TD165_ACTION_RING);
default:
return data;
}
}
bool IsSupported(PagerAction action) {
switch(action) {
case RING:
case MUTE:
case TURN_OFF_ALL:
return true;
default:
return false;
}
return false;
}
uint8_t GetActionsCount() {
return actionMask + 1;
}
};

View File

@@ -0,0 +1,97 @@
#pragma once
#include "PagerDecoder.hpp"
#define TD174_ACTION_RING 0
#define TD174_ACTION_DESYNC 3
#define TD174_PAGER_DESYNC 237
// Retekess TD174
class Td174Decoder : public PagerDecoder {
private:
const uint32_t stationMask = 0b111111111111100000000000; // leading 13 bits (of 24) are station
const uint32_t actionMask = 0b11000000000; // next 2 bits are action
const uint32_t pagerMask = 0b111111111; // and the last 9 bits is pager
const uint8_t stationBitCount = 13;
const uint8_t stationOffset = 11;
const uint8_t actionBitCount = 2;
const uint8_t actionOffset = 9;
const uint8_t pagerBitCount = 9;
public:
const char* GetShortName() {
return "TD174";
}
uint16_t GetStation(uint32_t data) {
uint32_t stationReversed = (data & stationMask) >> stationOffset;
return reverseBits(stationReversed, stationBitCount);
}
uint16_t GetPager(uint32_t data) {
uint32_t pagerReversed = data & pagerMask;
return reverseBits(pagerReversed, pagerBitCount);
}
uint8_t GetActionValue(uint32_t data) {
uint32_t actionReversed = (data & actionMask) >> actionOffset;
return reverseBits(actionReversed, actionBitCount);
}
PagerAction GetAction(uint32_t data) {
switch(GetActionValue(data)) {
case TD174_ACTION_RING:
return RING;
case TD174_ACTION_DESYNC:
if(GetPager(data) == TD174_PAGER_DESYNC) {
return DESYNC;
}
return UNKNOWN;
default:
return UNKNOWN;
}
}
uint32_t SetPager(uint32_t data, uint16_t pagerNum) {
return (data & ~pagerMask) | reverseBits(pagerNum, pagerBitCount);
}
uint32_t SetActionValue(uint32_t data, uint8_t actionValue) {
uint32_t actionCleared = data & ~actionMask;
return actionCleared | (reverseBits(actionValue, actionBitCount) << actionOffset);
}
uint32_t SetAction(uint32_t data, PagerAction action) {
switch(action) {
case RING:
return SetActionValue(data, TD174_ACTION_RING);
case DESYNC:
return SetActionValue(SetPager(data, TD174_PAGER_DESYNC), TD174_ACTION_DESYNC);
default:
return data;
}
}
bool IsSupported(PagerAction action) {
switch(action) {
case RING:
case DESYNC:
return true;
default:
return false;
}
return false;
}
uint8_t GetActionsCount() {
return 4;
}
};

View File

@@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
#include "lib/hardware/subghz/SubGhzPayload.hpp"
class PagerProtocol {
public:
uint8_t id;
virtual const char* GetSystemName() = 0;
virtual int GetFallbackTE() = 0;
virtual int GetMaxTE() = 0;
virtual SubGhzPayload* CreatePayload(uint64_t data, uint32_t te, uint32_t repeats) = 0;
virtual ~PagerProtocol() {
}
};

View File

@@ -0,0 +1,32 @@
#pragma once
#include <stream/stream.h>
#include "PagerProtocol.hpp"
class PrincetonProtocol : public PagerProtocol {
public:
const char* GetSystemName() {
return "Princeton";
}
int GetFallbackTE() {
return 212;
}
int GetMaxTE() {
return 1200;
}
SubGhzPayload* CreatePayload(uint64_t data, uint32_t te, uint32_t repeats) {
SubGhzPayload* payload = new SubGhzPayload(GetSystemName());
payload->SetBits(24);
payload->SetKey(data);
payload->SetTE(te);
// somewhy repeats are always 10 even if we set it, so use here "software repeats" instead
payload->SetSoftwareRepeats(ceil(repeats / 10.0));
payload->SetRepeat(10); // just in case they'll fix it
return payload;
}
};

View File

@@ -0,0 +1,26 @@
#pragma once
#include "PagerProtocol.hpp"
class Smc5326Protocol : public PagerProtocol {
const char* GetSystemName() {
return "SMC5326";
}
int GetFallbackTE() {
return 326;
}
int GetMaxTE() {
return 900;
}
SubGhzPayload* CreatePayload(uint64_t data, uint32_t te, uint32_t repeats) {
SubGhzPayload* payload = new SubGhzPayload(GetSystemName());
payload->SetBits(25);
payload->SetKey(data);
payload->SetTE(te);
payload->SetRepeat(repeats);
return payload;
}
};

View File

@@ -0,0 +1,33 @@
#pragma once
#include "lib/String.hpp"
#include "lib/ui/view/UiView.hpp"
#include "lib/ui/view/ProgressbarPopupUiView.hpp"
class BatchTransmissionScreen {
private:
ProgressbarPopupUiView* popup;
String statusStr;
public:
BatchTransmissionScreen(int pagersTotal) {
popup = new ProgressbarPopupUiView("Transmitting...");
SetProgress(0, pagersTotal);
popup->SetOnDestroyHandler(HANDLER(&BatchTransmissionScreen::destroy));
}
void SetProgress(int pagerNum, int pagersTotal) {
float progressValue = (float)pagerNum / pagersTotal;
popup->SetProgress(statusStr.format("Pager %d / %d", pagerNum, pagersTotal), progressValue);
}
private:
void destroy() {
delete this;
}
public:
UiView* GetView() {
return popup;
}
};

View File

@@ -0,0 +1,313 @@
#pragma once
#include "SelectCategoryScreen.hpp"
#include "lib/HandlerContext.hpp"
#include "lib/String.hpp"
#include "app/pager/PagerReceiver.hpp"
#include "lib/hardware/subghz/SubGhzModule.hpp"
#include "lib/ui/view/UiView.hpp"
#include "lib/ui/view/VariableItemListUiView.hpp"
#include "lib/ui/view/TextInputUiView.hpp"
#include "lib/ui/view/DialogUiView.hpp"
#include "lib/FlipperDolphin.hpp"
#include "lib/ui/UiManager.hpp"
#include "app/AppFileSystem.hpp"
#include "app/pager/PagerSerializer.hpp"
#define TE_DIV 10
class EditPagerScreen {
private:
AppConfig* config;
SubGhzModule* subghz;
PagerReceiver* receiver;
PagerDataGetter getPager;
VariableItemListUiView* varItemList;
UiVariableItem* encodingItem = NULL;
UiVariableItem* stationItem = NULL;
UiVariableItem* pagerItem = NULL;
UiVariableItem* actionItem = NULL;
UiVariableItem* hexItem = NULL;
UiVariableItem* protocolItem = NULL;
UiVariableItem* frequencyItem = NULL;
UiVariableItem* teItem = NULL;
UiVariableItem* repeatsItem = NULL;
UiVariableItem* saveAsItem = NULL;
UiVariableItem* deleteItem = NULL;
String stationStr;
String pagerStr;
String actionStr;
String hexStr;
String repeatsStr;
String frequencyStr;
String teStr;
int32_t saveAsItemIndex = -1;
int32_t deleteItemIndex = -1;
bool isFromFile;
const char* saveAsName = NULL;
public:
EditPagerScreen(
AppConfig* config,
SubGhzModule* subghz,
PagerReceiver* receiver,
PagerDataGetter pagerGetter,
bool isFromFile
) {
this->config = config;
this->subghz = subghz;
this->receiver = receiver;
this->getPager = pagerGetter;
this->isFromFile = isFromFile;
StoredPagerData* pager = getPager();
PagerDecoder* decoder = receiver->decoders[pager->decoder];
PagerProtocol* protocol = receiver->protocols[pager->protocol];
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
varItemList = new VariableItemListUiView();
varItemList->SetOnDestroyHandler(HANDLER(&EditPagerScreen::destroy));
varItemList->SetEnterPressHandler(HANDLER_1ARG(&EditPagerScreen::enterPressed));
varItemList->AddItem(
encodingItem = new UiVariableItem(
"Encoding", pager->decoder, receiver->decodersCount, HANDLER_1ARG(&EditPagerScreen::encodingValueChanged)
)
);
varItemList->AddItem(stationItem = new UiVariableItem("Station", HANDLER_1ARG(&EditPagerScreen::stationValueChanged)));
varItemList->AddItem(pagerItem = new UiVariableItem("Pager", HANDLER_1ARG(&EditPagerScreen::pagerValueChanged)));
updatePagerIsEditable();
varItemList->AddItem(
actionItem = new UiVariableItem(
"Action",
decoder->GetActionValue(pager->data),
decoder->GetActionsCount(),
HANDLER_1ARG(&EditPagerScreen::actionValueChanged)
)
);
varItemList->AddItem(hexItem = new UiVariableItem("HEX value", HANDLER_1ARG(&EditPagerScreen::hexValueChanged)));
varItemList->AddItem(protocolItem = new UiVariableItem("Protocol", protocol->GetSystemName()));
varItemList->AddItem(
frequencyItem = new UiVariableItem(
"Frequency", frequencyStr.format("%lu.%02lu", frequency / 1000000, (frequency % 1000000) / 10000)
)
);
varItemList->AddItem(
teItem = new UiVariableItem(
"TE", pager->te / TE_DIV, protocol->GetMaxTE() / TE_DIV, HANDLER_1ARG(&EditPagerScreen::teValueChanged)
)
);
varItemList->AddItem(
repeatsItem = new UiVariableItem(
"Signal Repeats", repeatsStr.format(pager->repeats == MAX_REPEATS ? "%d+" : "%d", pager->repeats)
)
);
if(canSave()) {
const char* saveAsItemName = isFromFile ? "Save / Rename" : "Save signal as...";
saveAsItemIndex = varItemList->AddItem(saveAsItem = new UiVariableItem(saveAsItemName, ""));
}
if(canDelete()) {
deleteItemIndex = varItemList->AddItem(deleteItem = new UiVariableItem("Delete station", ""));
}
}
private:
bool canSave() {
return !receiver->IsKnown(getPager()) || this->isFromFile;
}
bool canDelete() {
return isFromFile;
}
String* currentStationName() {
return receiver->GetName(getPager());
}
void updatePagerIsEditable() {
StoredPagerData* pager = getPager();
int pagerNum = receiver->decoders[pager->decoder]->GetPager(pager->data);
if(pagerNum < UINT8_MAX) {
pagerItem->SetSelectedItem(pagerNum, UINT8_MAX);
} else {
pagerItem->SetSelectedItem(0, 1);
}
}
void enterPressed(int32_t index) {
if(index == saveAsItemIndex) {
saveAs();
} else if(index == deleteItemIndex) {
DialogUiView* removeConfirmation = new DialogUiView("Really delete?", currentStationName()->cstr());
removeConfirmation->AddLeftButton("Nope");
removeConfirmation->AddRightButton("Yup");
removeConfirmation->SetResultHandler(HANDLER_1ARG(&EditPagerScreen::confirmDelete));
UiManager::GetInstance()->PushView(removeConfirmation);
} else {
transmitMessage();
}
}
void confirmDelete(DialogExResult result) {
if(result == DialogExResultRight) {
AppFileSysytem().DeletePager(receiver->GetCurrentUserCategory(), getPager());
receiver->ReloadKnownStations();
UiManager::GetInstance()->PopView(false);
}
}
void transmitMessage() {
StoredPagerData* pager = getPager();
PagerProtocol* protocol = receiver->protocols[pager->protocol];
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
subghz->Transmit(protocol->CreatePayload(pager->data, pager->te, config->SignalRepeats), frequency);
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
}
void saveAs() {
TextInputUiView* nameInputView = new TextInputUiView("Enter station name", NAME_MIN_LENGTH, NAME_MAX_LENGTH);
String* name = currentStationName();
if(name != NULL) {
nameInputView->SetDefaultText(name);
}
nameInputView->SetResultHandler(HANDLER_1ARG(&EditPagerScreen::saveAsHandler));
UiManager::GetInstance()->PushView(nameInputView);
}
void saveAsHandler(const char* name) {
saveAsName = name;
UiManager::GetInstance()->ShowLoading();
UiManager::GetInstance()->PushView(
(new SelectCategoryScreen(true, User, HANDLER_2ARG(&EditPagerScreen::categorySelected)))->GetView()
);
}
void categorySelected(CategoryType, const char* category) {
StoredPagerData* pager = getPager();
PagerDecoder* decoder = receiver->decoders[pager->decoder];
PagerProtocol* protocol = receiver->protocols[pager->protocol];
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
AppFileSysytem().SaveToUserCategory(category, saveAsName, pager, decoder, protocol, frequency);
FlipperDolphin::Deed(DolphinDeedSubGhzSave);
receiver->ReloadKnownStations();
for(int i = 0; i < 3; i++) {
UiManager::GetInstance()->PopView(false);
}
}
const char* encodingValueChanged(uint8_t index) {
StoredPagerData* pager = getPager();
PagerDecoder* decoder = receiver->decoders[index];
pager->decoder = index;
if(stationItem != NULL) {
stationItem->Refresh();
}
if(pagerItem != NULL) {
updatePagerIsEditable();
pagerItem->Refresh();
}
if(actionItem != NULL) {
actionItem->SetSelectedItem(decoder->GetActionValue(pager->data), decoder->GetActionsCount());
}
return receiver->decoders[pager->decoder]->GetShortName();
}
const char* stationValueChanged(uint8_t) {
StoredPagerData* pager = getPager();
return stationStr.fromInt(receiver->decoders[pager->decoder]->GetStation(pager->data));
}
const char* pagerValueChanged(uint8_t newPager) {
StoredPagerData* pager = getPager();
PagerDecoder* decoder = receiver->decoders[pager->decoder];
if(pagerItem->Editable() && newPager != decoder->GetPager(pager->data)) {
pager->data = decoder->SetPager(pager->data, newPager);
pager->edited = true;
if(hexItem != NULL) {
hexItem->Refresh();
}
}
return pagerStr.fromInt(decoder->GetPager(pager->data));
}
const char* actionValueChanged(uint8_t value) {
StoredPagerData* pager = getPager();
PagerDecoder* decoder = receiver->decoders[pager->decoder];
if(decoder->GetActionValue(pager->data) != value) {
pager->data = decoder->SetActionValue(pager->data, value);
pager->edited = true;
}
if(hexItem != NULL) {
hexItem->Refresh();
}
uint8_t actionValue = decoder->GetActionValue(pager->data);
PagerAction action = decoder->GetAction(pager->data);
const char* actionDesc = PagerActions::GetDescription(action);
return actionStr.format("%d (%s)", actionValue, actionDesc);
}
const char* hexValueChanged(uint8_t) {
StoredPagerData* pager = getPager();
return hexStr.format("%06X", (unsigned int)pager->data);
}
const char* teValueChanged(uint8_t newTeIndex) {
StoredPagerData* pager = getPager();
if(newTeIndex != pager->te / TE_DIV) {
int teDiff = pager->te % TE_DIV;
int newTe = newTeIndex * TE_DIV + teDiff;
pager->te = newTe;
pager->edited = true;
}
return teStr.format("%d", pager->te);
}
void destroy() {
delete encodingItem;
delete stationItem;
delete pagerItem;
delete actionItem;
delete hexItem;
delete frequencyItem;
delete teItem;
delete protocolItem;
delete repeatsItem;
if(saveAsItem != NULL) {
delete saveAsItem;
}
if(deleteItem != NULL) {
delete deleteItem;
}
delete this;
}
public:
UiView* GetView() {
return varItemList;
}
};

View File

@@ -0,0 +1,76 @@
#pragma once
#include "app/AppConfig.hpp"
#include "lib/ui/view/UiView.hpp"
#include "lib/ui/view/SubMenuUiView.hpp"
#include "lib/ui/UiManager.hpp"
#include "app/AppNotifications.hpp"
#include "SelectCategoryScreen.hpp"
#include "ScanStationsScreen.hpp"
class MainMenuScreen {
private:
AppConfig* config;
SubMenuUiView* menuView;
public:
MainMenuScreen(AppConfig* config) {
this->config = config;
menuView = new SubMenuUiView("Chief Cooker");
menuView->AddItem("Scan for station signals", HANDLER_1ARG(&MainMenuScreen::scanStationsMenuPressed));
menuView->AddItem("Saved stations database", HANDLER_1ARG(&MainMenuScreen::stationDatabasePressed));
menuView->AddItem("About / Manual", HANDLER_1ARG(&MainMenuScreen::aboutPressed));
menuView->SetOnDestroyHandler(HANDLER(&MainMenuScreen::destroy));
}
UiView* GetView() {
return menuView;
}
private:
void scanStationsMenuPressed(uint32_t) {
UiManager::GetInstance()->ShowLoading();
UiManager::GetInstance()->PushView((new ScanStationsScreen(config))->GetView());
}
void stationDatabasePressed(uint32_t) {
SubMenuUiView* savedMenuView = new SubMenuUiView("Select database");
savedMenuView->AddItem("Saved by you", HANDLER_1ARG(&MainMenuScreen::savedStationsPressed));
savedMenuView->AddItem("Autosaved", HANDLER_1ARG(&MainMenuScreen::autosavedStationsPressed));
UiManager::GetInstance()->PushView(savedMenuView);
}
void savedStationsPressed(uint32_t) {
UiManager::GetInstance()->ShowLoading();
UiManager::GetInstance()->PushView(
(new SelectCategoryScreen(false, User, HANDLER_2ARG(&MainMenuScreen::categorySelected)))->GetView()
);
}
void autosavedStationsPressed(uint32_t) {
UiManager::GetInstance()->ShowLoading();
UiManager::GetInstance()->PushView(
(new SelectCategoryScreen(false, Autosaved, HANDLER_2ARG(&MainMenuScreen::categorySelected)))->GetView()
);
}
void categorySelected(CategoryType categoryType, const char* category) {
UiManager::GetInstance()->ShowLoading();
UiManager::GetInstance()->PushView((new ScanStationsScreen(config, categoryType, category))->GetView());
}
void aboutPressed(uint32_t index) {
UNUSED(index);
Notification::Play(&NOTIFICATION_PAGER_RECEIVE);
menuView->SetItemLabel(index, "Developed by Denr01!");
}
void destroy() {
delete this;
}
};

View File

@@ -0,0 +1,160 @@
#pragma once
#include "lib/String.hpp"
#include "app/AppConfig.hpp"
#include "app/pager/data/StoredPagerData.hpp"
#include "app/pager/decoder/PagerDecoder.hpp"
#include "app/pager/protocol/PagerProtocol.hpp"
#include "lib/hardware/subghz/SubGhzModule.hpp"
#include "lib/ui/view/UiView.hpp"
#include "lib/ui/view/SubMenuUiView.hpp"
#include "app/screen/BatchTransmissionScreen.hpp"
#include "lib/FlipperDolphin.hpp"
#include "lib/ui/UiManager.hpp"
class PagerActionsScreen {
private:
AppConfig* config;
SubMenuUiView* submenu;
PagerDecoder* decoder;
PagerProtocol* protocol;
SubGhzModule* subghz;
PagerDataGetter getPager;
BatchTransmissionScreen* batchTransmissionScreen;
String headerStr;
String resendToAllStr;
String resendToCurrentStr;
String** actionsStrings;
uint32_t currentBatchFrequency;
uint32_t currentPager = 0;
bool transmittingBatch = false;
public:
PagerActionsScreen(
AppConfig* config,
PagerDataGetter pagerGetter,
PagerDecoder* decoder,
PagerProtocol* protocol,
SubGhzModule* subghz
) {
this->config = config;
this->getPager = pagerGetter;
this->decoder = decoder;
this->protocol = protocol;
this->subghz = subghz;
StoredPagerData* pager = getPager();
PagerAction currentAction = decoder->GetAction(pager->data);
uint8_t actionValue = decoder->GetActionValue(pager->data);
uint16_t stationNum = decoder->GetStation(pager->data);
uint16_t pagerNum = decoder->GetPager(pager->data);
submenu = new SubMenuUiView(headerStr.format("Station %d actions", stationNum));
submenu->SetOnDestroyHandler(HANDLER(&PagerActionsScreen::destroy));
submenu->SetOnReturnToViewHandler(HANDLER(&PagerActionsScreen::onReturn));
submenu->AddItem(
resendToAllStr.format("Resend %d (%s) to ALL", actionValue, PagerActions::GetDescription(currentAction)),
HANDLER_1ARG(&PagerActionsScreen::resendToAll)
);
if(currentAction == UNKNOWN) {
submenu->AddItem(
resendToCurrentStr.format("Resend only to pager %d", pagerNum), HANDLER_1ARG(&PagerActionsScreen::resendSingle)
);
}
actionsStrings = new String*[decoder->GetSupportedActionsCount()];
for(size_t actionIndex = 0, i = 0; actionIndex < PagerActionCount; actionIndex++) {
PagerAction action = static_cast<enum PagerAction>(actionIndex);
if(!decoder->IsSupported(action)) {
continue;
}
if(PagerActions::IsPagerActionSpecial(action)) {
actionsStrings[i] = new String("Trigger action %s", PagerActions::GetDescription(action));
} else {
actionsStrings[i] = new String("%s only pager %d", PagerActions::GetDescription(action), pagerNum);
}
submenu->AddItem(actionsStrings[i]->cstr(), [this, action](uint32_t) { sendAction(action); });
i++;
}
subghz->SetTransmitCompleteHandler(HANDLER(&PagerActionsScreen::txComplete));
}
private:
void resendToAll(uint32_t) {
currentPager = 0;
transmittingBatch = true;
currentBatchFrequency = FrequencyManager::GetInstance()->GetFrequency(getPager()->frequency);
batchTransmissionScreen = new BatchTransmissionScreen(config->MaxPagerForBatchOrDetection);
UiManager::GetInstance()->PushView(batchTransmissionScreen->GetView());
sendCurrentPager();
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
}
void resendSingle(uint32_t) {
StoredPagerData* pager = getPager();
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
subghz->Transmit(protocol->CreatePayload(pager->data, pager->te, config->SignalRepeats), frequency);
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
}
void sendAction(PagerAction action) {
StoredPagerData* pager = getPager();
uint32_t frequency = FrequencyManager::GetInstance()->GetFrequency(pager->frequency);
subghz->Transmit(
protocol->CreatePayload(decoder->SetAction(pager->data, action), pager->te, config->SignalRepeats), frequency
);
FlipperDolphin::Deed(DolphinDeedSubGhzSend);
}
void sendCurrentPager() {
StoredPagerData* pager = getPager();
batchTransmissionScreen->SetProgress(currentPager, config->MaxPagerForBatchOrDetection);
subghz->Transmit(
protocol->CreatePayload(decoder->SetPager(pager->data, currentPager), pager->te, config->SignalRepeats),
currentBatchFrequency
);
}
void txComplete() {
if(transmittingBatch) {
if(++currentPager <= config->MaxPagerForBatchOrDetection) {
sendCurrentPager();
return;
} else {
transmittingBatch = false;
UiManager::GetInstance()->PopView(false);
}
}
subghz->DefaultAfterTransmissionHandler();
}
void onReturn() {
transmittingBatch = false;
}
void destroy() {
subghz->SetTransmitCompleteHandler(NULL);
for(size_t i = 0; i < decoder->GetSupportedActionsCount(); i++) {
delete actionsStrings[i];
}
delete[] actionsStrings;
delete this;
}
public:
UiView* GetView() {
return submenu;
}
};

View File

@@ -0,0 +1,282 @@
#pragma once
#include "SettingsScreen.hpp"
#include "lib/hardware/subghz/data/SubGhzReceivedDataStub.hpp"
#include "lib/ui/view/ColumnOrientedListUiView.hpp"
#include "EditPagerScreen.hpp"
#include "PagerActionsScreen.hpp"
#include "lib/hardware/subghz/SubGhzModule.hpp"
#include "app/AppConfig.hpp"
#include "app/AppNotifications.hpp"
#include "app/pager/PagerReceiver.hpp"
static int8_t stationScreenColumnOffsets[]{
3, // station name (if known)
3, // hex
49, // station
72, // pager
94, // action
128 - 8 // repeats / edit flag
};
static Font stationScreenColumnFonts[]{
FontSecondary, // station name (if known)
FontBatteryPercent, // hex
FontSecondary, // station
FontSecondary, // pager
FontBatteryPercent, // action
FontBatteryPercent, // repeats
};
static Align stationScreenColumnAlignments[]{
AlignLeft, // station name (if known)
AlignLeft, // hex
AlignCenter, // station
AlignCenter, // pager
AlignCenter, // action
AlignRight, // repeats
};
class ScanStationsScreen {
private:
AppConfig* config;
ColumnOrientedListUiView* menuView;
PagerReceiver* pagerReceiver;
SubGhzModule* subghz;
bool receiveMode = false;
bool updateUserCategory = true;
int scanForMoreButtonIndex = -1;
uint32_t fromFilePagersCount = 0;
public:
ScanStationsScreen(AppConfig* config) : ScanStationsScreen(config, true, NotSelected, NULL) {
}
ScanStationsScreen(AppConfig* config, CategoryType categoryType, const char* category) :
ScanStationsScreen(config, false, categoryType, category) {
}
ScanStationsScreen(AppConfig* config, bool receiveNew, CategoryType categoryType, const char* category) {
this->config = config;
menuView = new ColumnOrientedListUiView(
stationScreenColumnOffsets,
sizeof(stationScreenColumnOffsets),
HANDLER_3ARG(&ScanStationsScreen::getElementColumnName)
);
menuView->SetOnDestroyHandler(HANDLER(&ScanStationsScreen::destroy));
menuView->SetOnReturnToViewHandler([this]() { this->menuView->Refresh(); });
menuView->SetGoBackHandler(HANDLER(&ScanStationsScreen::goBack));
menuView->SetColumnFonts(stationScreenColumnFonts);
menuView->SetColumnAlignments(stationScreenColumnAlignments);
menuView->SetLeftButton("Conf", HANDLER_1ARG(&ScanStationsScreen::showConfig));
subghz = new SubGhzModule(config->Frequency);
subghz->SetReceiveHandler(HANDLER_1ARG(&ScanStationsScreen::receive));
if(receiveNew) {
subghz->SetReceiveAfterTransmission(true);
subghz->ReceiveAsync();
}
pagerReceiver = new PagerReceiver(config);
if(categoryType == User) {
pagerReceiver->SetUserCategory(category);
updateUserCategory = false;
if(category != NULL) {
menuView->SetRightButton("Delete category", HANDLER_1ARG(&ScanStationsScreen::deleteCategory));
}
} else {
pagerReceiver->ReloadKnownStations();
}
if(receiveNew) {
if(subghz->IsExternal()) {
menuView->SetNoElementCaption("Receiving via EXT...");
} else {
menuView->SetNoElementCaption("Receiving...");
}
} else {
menuView->SetNoElementCaption("No stations found!");
}
if(!receiveNew) {
pagerReceiver->LoadStationsFromDirectory(categoryType, category, HANDLER_1ARG(&ScanStationsScreen::pagerAdded));
if(categoryType == User && menuView->GetElementsCount() > 0) {
scanForMoreButtonIndex = menuView->GetElementsCount();
fromFilePagersCount = menuView->GetElementsCount();
menuView->AddElement();
}
}
receiveMode = receiveNew;
}
UiView* GetView() {
return menuView;
}
private:
void receive(SubGhzReceivedData* data) {
pagerAdded(pagerReceiver->Receive(data));
delete data;
}
void pagerAdded(ReceivedPagerData* pagerData) {
if(pagerData != NULL) {
if(pagerData->IsNew()) {
if(receiveMode) {
Notification::Play(&NOTIFICATION_PAGER_RECEIVE);
}
if(pagerData->GetIndex() == 0) { // add buttons after capturing the first transmission
menuView->SetCenterButton("Actions", HANDLER_1ARG(&ScanStationsScreen::showActions));
menuView->SetRightButton("Edit", HANDLER_1ARG(&ScanStationsScreen::editPagerMessage));
}
if(!receiveMode || scanForMoreButtonIndex == -1) {
menuView->AddElement();
} else {
scanForMoreButtonIndex = -1;
}
}
if(menuView->IsOnTop()) {
menuView->Refresh();
}
delete pagerData;
}
}
void getElementColumnName(int index, int column, String* str) {
StoredPagerData* pagerData = pagerReceiver->PagerGetter(index)();
PagerDecoder* decoder = pagerReceiver->decoders[pagerData->decoder];
String* name = pagerReceiver->GetName(pagerData);
if(index == scanForMoreButtonIndex) {
if(column == 0) {
if(!receiveMode) {
str->format("> Scan here for more");
} else {
str->format("Scanning...");
}
}
return;
}
switch(column) {
case 0: // station name
if(name != NULL) {
str->format("%s", name->cstr());
}
break;
case 1: // hex
if(name == NULL) {
str->format("%06X", pagerData->data);
}
break;
case 2: // station
if(name == NULL) {
str->format("%d", decoder->GetStation(pagerData->data));
}
break;
case 3: // pager
str->format("%d", decoder->GetPager(pagerData->data));
break;
case 4: // action
{
PagerAction action = decoder->GetAction(pagerData->data);
if(action == UNKNOWN) {
str->format("%d", decoder->GetActionValue(pagerData->data));
} else {
str->format("%.4s", PagerActions::GetDescription(action));
}
}; break;
case 5: // repeats or edit flag
if(pagerData->edited) {
str->format("**");
} else if(receiveMode) {
str->format("x%d", pagerData->repeats);
}
break;
default:
break;
}
}
void showConfig(uint32_t) {
SettingsScreen* screen = new SettingsScreen(config, pagerReceiver, subghz, updateUserCategory);
UiManager::GetInstance()->PushView(screen->GetView());
}
void editPagerMessage(uint32_t index) {
if((int)index == scanForMoreButtonIndex) {
return;
}
PagerDataGetter getPager = pagerReceiver->PagerGetter(index);
EditPagerScreen* screen = new EditPagerScreen(config, subghz, pagerReceiver, getPager, index < fromFilePagersCount);
UiManager::GetInstance()->PushView(screen->GetView());
}
void showActions(uint32_t index) {
if((int)index == scanForMoreButtonIndex) {
if(!receiveMode) {
subghz->SetReceiveAfterTransmission(true);
subghz->ReceiveAsync();
receiveMode = true;
}
return;
}
PagerDataGetter getPager = pagerReceiver->PagerGetter(index);
StoredPagerData* pagerData = getPager();
PagerDecoder* decoder = pagerReceiver->decoders[pagerData->decoder];
PagerProtocol* protocol = pagerReceiver->protocols[pagerData->protocol];
PagerActionsScreen* screen = new PagerActionsScreen(config, getPager, decoder, protocol, subghz);
UiManager::GetInstance()->PushView(screen->GetView());
}
bool goBack() {
if(receiveMode && menuView->GetElementsCount() > 0) {
DialogUiView* confirmGoBack = new DialogUiView("Really stop scan?", "You may loose captured signals");
confirmGoBack->AddLeftButton("No");
confirmGoBack->AddRightButton("Yes");
confirmGoBack->SetResultHandler(HANDLER_1ARG(&ScanStationsScreen::goBackConfirmationHandler));
UiManager::GetInstance()->PushView(confirmGoBack);
return false;
}
return true;
}
void deleteCategory(int) {
AppFileSysytem().DeleteCategory(pagerReceiver->GetCurrentUserCategory());
menuView->SetRightButton("Deleted", NULL);
}
void goBackConfirmationHandler(DialogExResult dialogResult) {
if(dialogResult == DialogExResultRight) {
UiManager::GetInstance()->PopView(false);
}
}
void destroy() {
delete subghz;
delete pagerReceiver;
delete this;
}
};

View File

@@ -0,0 +1,88 @@
#pragma once
#include "app/AppFileSystem.hpp"
#include "lib/ui/UiManager.hpp"
#include "lib/ui/view/SubMenuUiView.hpp"
#include "lib/ui/view/TextInputUiView.hpp"
#define MIN_CAT_NAME_LENGTH 2
#define MAX_CAT_NAME_LENGTH MAX_FILENAME_LENGTH
class SelectCategoryScreen {
private:
SubMenuUiView* menu;
CategoryType categoryType;
forward_list<char*> categories;
function<void(CategoryType, const char*)> categorySelectedHandler;
TextInputUiView* nameInput;
char* categoryAddedName;
public:
SelectCategoryScreen(
bool canCreateNew,
CategoryType categoryType,
function<void(CategoryType, const char*)> categorySelectedHandler
) {
this->categoryType = categoryType;
this->categorySelectedHandler = categorySelectedHandler;
menu = new SubMenuUiView("Select category");
menu->SetOnDestroyHandler(HANDLER(&SelectCategoryScreen::destory));
if(canCreateNew) {
menu->AddItem("+ Create NEW", HANDLER_1ARG(&SelectCategoryScreen::createNew));
}
if(categoryType == User) {
menu->AddItem("<Default/Uncategorized>", [categoryType, categorySelectedHandler](uint32_t) {
return categorySelectedHandler(categoryType, NULL);
});
}
AppFileSysytem().GetCategories(&categories, categoryType);
for(char* category : categories) {
addCategory(category);
}
}
UiView* GetView() {
return menu;
}
private:
void createNew(uint32_t) {
if(categoryAddedName != NULL) {
categorySelectedHandler(categoryType, categoryAddedName);
return;
}
if(nameInput == NULL) {
nameInput = new TextInputUiView("Enter category name", MIN_CAT_NAME_LENGTH, MAX_CAT_NAME_LENGTH);
nameInput->SetOnDestroyHandler([this]() { this->nameInput = NULL; });
nameInput->SetResultHandler(HANDLER_1ARG(&SelectCategoryScreen::addAndSelectCategory));
}
UiManager::GetInstance()->PushView(nameInput);
}
void addCategory(char* name) {
menu->AddItem(name, [this, name](uint32_t) { return this->categorySelectedHandler(this->categoryType, name); });
}
void addAndSelectCategory(char* name) {
categoryAddedName = name;
menu->SetItemLabel(0, name);
UiManager::GetInstance()->PopView(true);
}
void destory() {
while(!categories.empty()) {
delete[] categories.front();
categories.pop_front();
}
if(nameInput != NULL) {
delete nameInput;
}
delete this;
}
};

View File

@@ -0,0 +1,177 @@
#pragma once
#include "SelectCategoryScreen.hpp"
#include "app/AppConfig.hpp"
#include "app/pager/PagerReceiver.hpp"
#include "lib/String.hpp"
#include "lib/hardware/subghz/SubGhzModule.hpp"
#include "lib/ui/UiManager.hpp"
#include "lib/ui/view/VariableItemListUiView.hpp"
class SettingsScreen {
private:
AppConfig* config;
SubGhzModule* subghz;
PagerReceiver* receiver;
VariableItemListUiView* varItemList;
UiVariableItem* currentCategoryItem;
UiVariableItem* frequencyItem;
UiVariableItem* maxPagerItem;
UiVariableItem* signalRepeatItem;
UiVariableItem* ignoreSavedItem;
UiVariableItem* autosaveFoundItem;
UiVariableItem* debugModeItem;
String frequencyStr;
String maxPagerStr;
String signalRepeatStr;
bool updateUserCategory;
uint32_t categoryItemIndex;
public:
SettingsScreen(AppConfig* config, PagerReceiver* receiver, SubGhzModule* subghz, bool updateUserCategory) {
this->config = config;
this->receiver = receiver;
this->subghz = subghz;
this->updateUserCategory = updateUserCategory;
varItemList = new VariableItemListUiView();
varItemList->SetOnDestroyHandler(HANDLER(&SettingsScreen::destroy));
varItemList->SetEnterPressHandler(HANDLER_1ARG(&SettingsScreen::enterPressHandler));
categoryItemIndex = varItemList->AddItem(
currentCategoryItem = new UiVariableItem("Category", HANDLER_1ARG(&SettingsScreen::categoryChangedHandler))
);
varItemList->AddItem(
frequencyItem = new UiVariableItem(
"Scan frequency",
FrequencyManager::GetInstance()->GetFrequencyIndex(config->Frequency),
FrequencyManager::GetInstance()->GetFrequencyCount(),
[this](uint8_t val) {
uint32_t freq = this->config->Frequency = FrequencyManager::GetInstance()->GetFrequency(val);
this->subghz->SetReceiveFrequency(this->config->Frequency);
return frequencyStr.format("%lu.%02lu", freq / 1000000, (freq % 1000000) / 10000);
}
)
);
varItemList->AddItem(
maxPagerItem = new UiVariableItem(
"Max pager value",
config->MaxPagerForBatchOrDetection - 1,
UINT8_MAX,
[this](uint8_t val) {
this->config->MaxPagerForBatchOrDetection = val + 1;
return maxPagerStr.fromInt(this->config->MaxPagerForBatchOrDetection);
}
)
);
varItemList->AddItem(
signalRepeatItem = new UiVariableItem(
"Times to repeat signal",
config->SignalRepeats - 1,
UINT8_MAX,
[this](uint8_t val) {
this->config->SignalRepeats = val + 1;
return signalRepeatStr.fromInt(this->config->SignalRepeats);
}
)
);
varItemList->AddItem(
ignoreSavedItem = new UiVariableItem(
"Saved stations",
config->SavedStrategy,
SavedStationStrategyValuesCount,
[this](uint8_t val) {
this->config->SavedStrategy = static_cast<enum SavedStationStrategy>(val);
return savedStationsStrategy(this->config->SavedStrategy);
}
)
);
varItemList->AddItem(
autosaveFoundItem = new UiVariableItem(
"Autosave found signals",
config->AutosaveFoundSignals,
2,
[this](uint8_t val) {
this->config->AutosaveFoundSignals = val;
return boolOption(val);
}
)
);
}
UiView* GetView() {
return varItemList;
}
private:
void enterPressHandler(uint32_t index) {
if(index != categoryItemIndex) {
return;
}
UiManager::GetInstance()->PushView(
(new SelectCategoryScreen(false, User, HANDLER_2ARG(&SettingsScreen::categorySelected)))->GetView()
);
}
void categorySelected(CategoryType, const char* category) {
if(config->CurrentUserCategory != NULL) {
delete config->CurrentUserCategory;
}
config->CurrentUserCategory = category != NULL ? new String("%s", category) : NULL;
UiManager::GetInstance()->PopView(false);
currentCategoryItem->Refresh();
}
const char* categoryChangedHandler(uint8_t) {
const char* category = config->GetCurrentUserCategoryCstr();
if(category == NULL) {
category = "Default";
}
return category;
}
const char* boolOption(uint8_t value) {
return value ? "ON" : "OFF";
}
const char* savedStationsStrategy(SavedStationStrategy value) {
switch(value) {
case IGNORE:
return "Ignore";
case SHOW_NAME:
return "Show name";
case HIDE:
return "Hide";
default:
return NULL;
}
}
void destroy() {
config->Save();
if(updateUserCategory) {
receiver->SetUserCategory(config->CurrentUserCategory);
receiver->ReloadKnownStations();
}
delete currentCategoryItem;
delete frequencyItem;
delete maxPagerItem;
delete signalRepeatItem;
delete ignoreSavedItem;
delete autosaveFoundItem;
delete debugModeItem;
delete this;
}
};

View File

@@ -0,0 +1,17 @@
# For details & more options, see documentation/AppManifests.md in firmware repo
App(
appid="chief_cooker", # Must be unique
name="Chief Cooker", # Displayed in menus
apptype=FlipperAppType.EXTERNAL,
entry_point="chief_cooker_app",
stack_size=2 * 1024,
fap_category="Sub-GHz",
# Optional values
# fap_version="0.1",
fap_icon="chief_cooker.png", # 10x10 1-bit PNG
# fap_description="A simple app",
fap_author="Denr01",
fap_weburl="https://github.com/denr01/FZ-ChiefCooker",
fap_icon_assets="images", # Image assets to compile for this application
)

View File

@@ -0,0 +1,13 @@
/* generated by fbt from .png files in images folder */
#include <chief_cooker_icons.h>
#include "app/App.hpp"
extern "C" int32_t chief_cooker_app(void* p) {
UNUSED(p);
App app;
app.Run();
return 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

View File

@@ -0,0 +1,127 @@
# Usage instructions
## Installation
To install this app simply download the `.fap` file from [latest release](https://github.com/denr01/FZ-ChiefCooker/releases/latest).
Then just copy it to your flipper (to `ext/apps/Sub-GHz` folder).
On your flipper, open up Apps -> Sub-GHz and you should see it there. Just open it as a regular app.
## Tutorial
### Your first use
Imagine you are on a food court you want to become chief on.
First, open the app and select "Scan for station signals".
The app will start receiving signals and show you once it receives something
<img src="screenshots/main-scan.png" width="256"> <img src="screenshots/scan-empty.png" width="256"> <img src="screenshots/scan-capture1.png" width="256">
Okay, now you received something. Now, let's test if transmission decoded correctly and find the restaurant who sent the transmission!
To do this, click on center button (actions) and select the first one, "Resend to ALL":
<img src="screenshots/scan-capture1-actions.png" width="256"> <img src="screenshots/scan-capture1-resend.png" alt="Description" width="256">
Where are they all running? Is the dinner ready yet? Unfortunately it's not. Just their new chief is learning...
Let's assume that you somehow found out, that it was a restaurant called "Street Food" who sent the signal. Now let's save it's signal to your SD card.
Go back from actions and push the "Edit >" (right arrow) button. Then scroll down to "Save signal as...", give it a name and then create a new category for it. It's convenient to use restaurant name for signal name and mall (or food court/place name where restaurant are located) for the category name to make sure that signals from different places will not mess up.
<img src="screenshots/scan-capture1.png" width="256"> <img src="screenshots/scan-capture1-edit-save.png" width="256"> <img src="screenshots/scan-capture1-save-name.png" width="256">
<img src="screenshots/scan-capture1-categories.png" width="256"> <img src="screenshots/scan-capture1-category-name.png" width="256"> <img src="screenshots/scan-capture1-categories-with-new.png" width="256">
Congratulations! Your saved your first captured signal and now can use it anytime you want to call someone to the restaurant's food pickup.
But it would be great now to distinguish the signals from "Street Food" from the other ones. And you can do it!
Navigate to "< Config" (left button) and select the category to the newly created one. Then go back.
<img src="screenshots/scan-capture1.png" width="256"> <img src="screenshots/scan-capture1-config.png" width="256"> <img src="screenshots/scan-capture1-conf-select-cat.png" width="256">
<img src="screenshots/scan-capture1-conf-cat-selected.png" width="256"> <img src="screenshots/scan-capture1-with-name.png" width="256">
As you can see, now instead of hex value and station number there is a restaurant name you given to the signal.
And what's this? A new signal? Yes, but not completely new. Street Food just called pager with another number (7). But your flipper successfully recognized their signal because you already saved one to the current category and showed you it's name.
<img src="screenshots/scan-capture2.png" width="256">
### Your second use
Now imagine you came to the same food court next day and want to call somebody's pager at the Street Food restaurant (that your saved yesterday).
Navigate to "Saved stations" and then select the category of current place / mall / food court:
<img src="screenshots/main-saved.png" width="256"> <img src="screenshots/saved-by-you.png" width="256"> <img src="screenshots/scan-capture1-categories-with-new.png" width="256">
Here your will see your saved signal:
<img src="screenshots/saved-category.png" width="256">
Now your can do with it whatever you want exactly like when you captured it.
Let's assume now you need to call only single pager with number 9.
Go to "Edit >" menu, scroll down to "Pager" and change it value to 9. Now just press center button. Your flipper will blink purple LED - like when you were resending to all pagers, remember? This means that it sent the signal.
<img src="screenshots/pager-9.png" width="256">
Now imagine we want to receive more signals here. There are two ways to do it:
- Go to "Scan" menu like on your first usage
- Click on "Scan here for more".
The second way is better because you will have all you previously saved signals in quick access in cause you urgently need one of them. New signals will appear here once your flipper receive them.
<img src="screenshots/saved-category-scan-here.png" width="256"> <img src="screenshots/saved-category-scanning.png" width="256"> <img src="screenshots/saved-category-scanning-new.png" width="256">
Congratulations! You have successfully completed the tutorial! ~~Now go and troll someone real.~~
## App's screens explanation
### Scan stations screen
<img src="screenshots/scan-capture1.png" width="256">
The values here are:
- `CBC042` - signal hex code
- `815` - station number (in current encoding)
- `4` - pager number (in current encoding)
- `RING` - action (in current encoding)
- `x8` - number of signal repeats, will not show more than `x99`
_Note: if you change the signal's encoding in "Edit" menu, station number, pager and action here will also change._
### Edit station screen
There are several things you can edit in captured signal using "Edit >" menu.
First thing is **encoding**. App tries to detect encoding automatically when it receives signal. But in some cases you may need to specify it manually. Here you can change the encoding and see how the values (station number, pager number and action) are changing in real-time:
<img src="screenshots/edit-decode-1.png" width="256"> <img src="screenshots/edit-decode-2.png" width="256"> <img src="screenshots/edit-decode-3.png" width="256">
Also you may need to change pager or action value. You can do it here and see how the hex value changes in real time:
<img src="screenshots/edit-pager-1.png" width="256"> <img src="screenshots/edit-pager-2.png" width="256">
_Note: pager number is editable only if it's decoded value is less than 255 in current encoding_
**Also note: pressing the center button on anywhere on the edit screen (except for save as / delete options) will trigger signal transmission with current pager/action/hex value!**
### Config screen
<img src="screenshots/scan-capture1-config.png" width="256">
Here some description about config parameters:
- **Category** - the category to load saved station names from. Does not affect if you use option "Scan here for more" in saved stations screen.
- **Scan frequency** - the frequency to receive signals on. For EU/Russia default is 433.92 Mhz, but 315.00 Mhz and 467.75 Mhz may be also used in US or somewhere else.
- **Max pager value** - how many pagers should signal be sent to when using "Resend to ALL" action. Also affects automatic encoding detection feature: the algorithm will use the first encoding which will give a pager number less or equal than current setting value.
- **Times to repeat signal** - speaks for itself, don't recommend changing it as the default value (10) should work in most cases.
- **Saved stations** - what to do when receive a signal from known station (saved in current category). Possible values are:
1. **Ignore** - treat station as unknown, show signal hex and station number.
2. **Show name** (default) - show saved station name instead of hex value and station number
3. **Hide** - do not show signals from saved stations at all. Show only unknown signals
- **Autosave found signals** - any found signals will be saved to "Autosaved" folder in the subdirectory with current date. Useful in case app crashes or you accidentally close it without saving.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,11 @@
#pragma once
#include <dolphin/dolphin.h>
class FlipperDolphin {
private:
public:
static void Deed(DolphinDeed deed) {
dolphin_deed(deed);
}
};

View File

@@ -0,0 +1,36 @@
#pragma once
#define HANDLER(handlerMethod) bind(handlerMethod, this)
#define HANDLER_1ARG(handlerMethod) bind(handlerMethod, this, placeholders::_1)
#define HANDLER_2ARG(handlerMethod) bind(handlerMethod, this, placeholders::_1, placeholders::_2)
#define HANDLER_3ARG(handlerMethod) bind(handlerMethod, this, placeholders::_1, placeholders::_2, placeholders::_3)
template <class T>
class HandlerContext {
private:
T handler;
public:
HandlerContext(T handler) {
this->handler = handler;
}
T GetHandler() {
return handler;
}
};
template <class T>
class HandlerContextExt : public HandlerContext<T> {
private:
void* extContext;
public:
HandlerContextExt(T handler, void* extContext) : HandlerContext<T>(handler) {
this->extContext = extContext;
}
void* GetExtContext() {
return extContext;
}
};

View File

@@ -0,0 +1,53 @@
#pragma once
#include <core/string.h>
class String {
private:
FuriString* string;
public:
String() {
string = furi_string_alloc();
}
String(const char* format, ...) {
va_list args;
va_start(args, format);
string = furi_string_alloc_vprintf(format, args);
va_end(args);
}
FuriString* furiString() {
return string;
}
const char* cstr() {
return furi_string_get_cstr(string);
}
const char* fromInt(int value) {
return format("%d", value);
}
const char* format(const char* format, ...) {
va_list args;
va_start(args, format);
furi_string_vprintf(string, format, args);
va_end(args);
return cstr();
}
bool isEmpty() {
return furi_string_empty(string);
}
void Reset() {
furi_string_reset(string);
}
~String() {
furi_string_free(string);
}
};

View File

@@ -0,0 +1,63 @@
#pragma once
#include <toolbox/path.h>
#include <storage/storage.h>
class Directory {
private:
bool isOpened;
Storage* storage;
File* dir;
public:
Directory(Storage* storage, const char* dirPath) {
this->storage = storage;
dir = storage_file_alloc(storage);
isOpened = storage_dir_open(dir, dirPath);
}
bool IsOpened() {
return isOpened;
}
void Rewind() {
if(isOpened) {
storage_dir_rewind(dir);
}
}
bool GetNextFile(char* name, uint16_t nameLength) {
if(!isOpened) {
return false;
}
FileInfo fileInfo = FileInfo();
do {
if(!storage_dir_read(dir, &fileInfo, name, nameLength)) {
return false;
}
} while((fileInfo.flags & FSF_DIRECTORY) > 0); // dir
return true;
}
bool GetNextDir(char* name, uint16_t nameLength) {
if(!isOpened) {
return false;
}
FileInfo fileInfo = FileInfo();
do {
if(!storage_dir_read(dir, &fileInfo, name, nameLength)) {
return false;
}
} while((fileInfo.flags & FSF_DIRECTORY) == 0); // not dir
return true;
}
~Directory() {
storage_dir_close(dir);
storage_file_free(dir);
}
};

View File

@@ -0,0 +1,74 @@
#pragma once
#include "lib/String.hpp"
#include <toolbox/path.h>
#include <storage/storage.h>
#include "FlipperFile.hpp"
#include "Directory.hpp"
class FileManager {
private:
Storage* storage;
public:
FileManager() {
storage = (Storage*)furi_record_open(RECORD_STORAGE);
}
void CreateDirIfNotExists(const char* path) {
if(!storage_dir_exists(storage, path)) {
storage_common_mkdir(storage, path);
}
}
Directory* OpenDirectory(const char* path) {
Directory* dir = new Directory(storage, path);
if(dir->IsOpened()) {
return dir;
}
delete dir;
return NULL;
}
FlipperFile* OpenRead(const char* path) {
FlipperFile* file = new FlipperFile(storage, path, false);
if(file->IsOpened()) {
return file;
}
delete file;
return NULL;
}
FlipperFile* OpenRead(const char* dir, const char* file) {
String concatedPath = String("%s/%s", dir, file);
return OpenRead(concatedPath.cstr());
}
FlipperFile* OpenWrite(const char* path) {
FlipperFile* file = new FlipperFile(storage, path, true);
if(file->IsOpened()) {
return file;
}
delete file;
return NULL;
}
FlipperFile* OpenWrite(const char* dir, const char* file) {
String concatedPath = String("%s/%s", dir, file);
return OpenWrite(concatedPath.cstr());
}
void DeleteFile(const char* dir, const char* file) {
String concatedPath = String("%s/%s", dir, file);
DeleteFile(concatedPath.cstr());
}
void DeleteFile(const char* filePath) {
storage_common_remove(storage, filePath);
}
~FileManager() {
furi_record_close(RECORD_STORAGE);
}
};

View File

@@ -0,0 +1,61 @@
#pragma once
#include "flipper_format.h"
#include "lib/String.hpp"
class FlipperFile {
private:
bool isOpened;
FlipperFormat* flipperFormat;
public:
FlipperFile(Storage* storage, const char* path, bool write) {
flipperFormat = flipper_format_file_alloc(storage);
if(write) {
isOpened = flipper_format_file_open_always(flipperFormat, path);
} else {
isOpened = flipper_format_file_open_existing(flipperFormat, path);
}
}
bool IsOpened() {
return isOpened;
}
bool ReadUInt32(const char* key, uint32_t* valueTarget) {
return flipper_format_read_uint32(flipperFormat, key, valueTarget, 1);
}
bool ReadBool(const char* key, bool* valueTarget) {
return flipper_format_read_bool(flipperFormat, key, valueTarget, 1);
}
bool ReadString(const char* key, String* valueTarget) {
return flipper_format_read_string(flipperFormat, key, valueTarget->furiString());
}
bool ReadHex(const char* key, uint64_t* value) {
return flipper_format_read_hex(flipperFormat, key, (uint8_t*)value, sizeof(value));
}
bool WriteUInt32(const char* key, uint32_t value) {
return flipper_format_write_uint32(flipperFormat, key, &value, 1);
}
bool WriteBool(const char* key, bool value) {
return flipper_format_write_bool(flipperFormat, key, &value, 1);
}
bool WriteString(const char* key, const char* value) {
return flipper_format_write_string_cstr(flipperFormat, key, value);
}
bool WriteHex(const char* key, uint64_t value) {
return flipper_format_write_hex(flipperFormat, key, (const uint8_t*)&value, sizeof(value));
}
~FlipperFile() {
flipper_format_file_close(flipperFormat);
flipper_format_free(flipperFormat);
}
};

View File

@@ -0,0 +1,29 @@
#pragma once
#include <furi.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
static NotificationApp* __notification_app_instance = NULL;
class Notification {
private:
static NotificationApp* getApp() {
if(__notification_app_instance == NULL) {
__notification_app_instance = (NotificationApp*)furi_record_open(RECORD_NOTIFICATION);
}
return __notification_app_instance;
}
public:
static void Play(const NotificationSequence* nullTerminatedSequence) {
notification_message(getApp(), nullTerminatedSequence);
}
static void Dispose() {
if(__notification_app_instance != NULL) {
furi_record_close(RECORD_NOTIFICATION);
__notification_app_instance = NULL;
}
}
};

View File

@@ -0,0 +1,66 @@
#pragma once
#include "lib/subghz/subghz_setting.h"
static void* __freq_manager_instance = NULL;
class FrequencyManager {
private:
uint32_t* frequencies;
uint8_t frequencyCount;
uint8_t defaultFreqIndex;
FrequencyManager() {
SubGhzSetting* setting = subghz_setting_alloc();
subghz_setting_load(setting, EXT_PATH("subghz/assets/setting_user"));
frequencyCount = subghz_setting_get_frequency_count(setting);
defaultFreqIndex = subghz_setting_get_frequency_default_index(setting);
frequencies = new uint32_t[frequencyCount];
for(int i = 0; i < frequencyCount; i++) {
frequencies[i] = subghz_setting_get_frequency(setting, i);
}
subghz_setting_free(setting);
}
public:
static FrequencyManager* GetInstance() {
if(__freq_manager_instance == NULL) {
__freq_manager_instance = new FrequencyManager();
}
return (FrequencyManager*)__freq_manager_instance;
}
uint32_t GetFrequency(size_t index) {
return frequencies[index];
}
uint32_t GetDefaultFrequency() {
return frequencies[defaultFreqIndex];
}
size_t GetDefaultFrequencyIndex() {
return defaultFreqIndex;
}
size_t GetFrequencyIndex(uint32_t freq) {
for(size_t i = 0; i < GetFrequencyCount(); i++) {
if(GetFrequency(i) == freq) {
return i;
}
}
return GetDefaultFrequencyIndex();
}
size_t GetFrequencyCount() {
return frequencyCount;
}
~FrequencyManager() {
if(frequencies != NULL) {
delete[] frequencies;
}
}
};

View File

@@ -0,0 +1,287 @@
#pragma once
#include "lib/hardware/subghz/SubGhzPayload.hpp"
#include <functional>
#include <furi.h>
#include <furi_hal.h>
#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
#include <lib/subghz/receiver.h>
#include <lib/subghz/transmitter.h>
#include <lib/subghz/devices/devices.h>
#include <lib/subghz/devices/cc1101_configs.h>
#include <lib/subghz/subghz_protocol_registry.h>
#include <lib/subghz/subghz_worker.h>
#include "SubGhzState.hpp"
#include "data/SubGhzReceivedDataImpl.hpp"
#include "lib/hardware/notification/Notification.hpp"
using namespace std;
#undef LOG_TAG
#define LOG_TAG "SUB_GHZ"
class SubGhzModule {
private:
SubGhzEnvironment* environment;
const SubGhzDevice* device;
SubGhzReceiver* receiver;
SubGhzWorker* worker;
SubGhzTransmitter* transmitter;
function<void(SubGhzReceivedData*)> receiveHandler;
FuriTimer* txCompleteCheckTimer;
function<void()> txCompleteHandler;
int repeatsLeft = 0;
SubGhzPayload* currentPayload;
uint32_t receiveFrequency = 0;
bool isExternal;
SubGhzState state = IDLE;
bool receiveAfterTransmission = false;
static void captureCallback(SubGhzReceiver* receiver, SubGhzProtocolDecoderBase* decoderBase, void* context) {
UNUSED(receiver);
if(context == NULL) {
return;
}
SubGhzModule* subghz = (SubGhzModule*)context;
if(subghz->receiveHandler != NULL) {
subghz->receiveHandler(new SubGhzReceivedDataImpl(decoderBase, subghz->receiveFrequency));
}
}
static void txCompleteCheckCallback(void* context) {
SubGhzModule* subghz = (SubGhzModule*)context;
if(subghz_devices_is_async_complete_tx(subghz->device)) {
if(subghz->repeatsLeft-- > 0 && subghz->currentPayload != NULL) {
subghz->startTransmission(0);
return;
}
furi_timer_stop(subghz->txCompleteCheckTimer);
if(subghz->txCompleteHandler != NULL) {
subghz->txCompleteHandler();
} else {
subghz->DefaultAfterTransmissionHandler();
}
}
}
void prepareReceiver() {
receiver = subghz_receiver_alloc_init(environment);
subghz_receiver_set_filter(receiver, SubGhzProtocolFlag_Decodable);
subghz_receiver_set_rx_callback(receiver, captureCallback, this);
worker = subghz_worker_alloc();
subghz_worker_set_overrun_callback(worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset);
subghz_worker_set_pair_callback(worker, (SubGhzWorkerPairCallback)subghz_receiver_decode);
subghz_worker_set_context(worker, receiver);
}
void setFrequencyIgnoringStateChecks(uint32_t frequency) {
if(subghz_devices_is_frequency_valid(device, frequency)) {
subghz_devices_set_frequency(device, frequency);
}
}
public:
SubGhzModule(uint32_t frequency) {
environment = subghz_environment_alloc();
subghz_environment_set_protocol_registry(environment, &subghz_protocol_registry);
subghz_devices_init();
furi_hal_power_enable_otg();
device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
if(!subghz_devices_is_connect(device)) {
furi_hal_power_disable_otg();
device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
isExternal = false;
} else {
isExternal = true;
}
subghz_devices_begin(device);
subghz_devices_load_preset(device, FuriHalSubGhzPresetOok650Async, NULL);
SetReceiveFrequency(frequency);
txCompleteCheckTimer = furi_timer_alloc(txCompleteCheckCallback, FuriTimerTypePeriodic, this);
}
void SetReceiveFrequency(uint32_t frequency) {
if(receiveFrequency == frequency) {
return;
} else {
receiveFrequency = frequency;
}
bool restoreReceive = state == RECEIVING;
PutToIdle();
setFrequencyIgnoringStateChecks(frequency);
if(restoreReceive) {
ReceiveAsync();
}
}
void SetReceiveAfterTransmission(bool value) {
this->receiveAfterTransmission = value;
}
void DefaultAfterTransmissionHandler() {
if(receiveAfterTransmission) {
ReceiveAsync();
} else {
PutToIdle();
}
}
void SetReceiveHandler(function<void(SubGhzReceivedData*)> handler) {
receiveHandler = handler;
}
void ReceiveAsync() {
if(receiver == NULL) {
prepareReceiver();
}
PutToIdle();
setFrequencyIgnoringStateChecks(receiveFrequency);
subghz_devices_flush_rx(device);
subghz_devices_start_async_rx(device, (void*)subghz_worker_rx_callback, worker);
subghz_worker_start(worker);
state = RECEIVING;
}
void SetTransmitCompleteHandler(function<void()> txCompleteHandler) {
this->txCompleteHandler = txCompleteHandler;
}
void Transmit(SubGhzPayload* payload, uint32_t frequency) {
if(state != TRANSMITTING) {
PutToIdle();
state = TRANSMITTING;
} else {
furi_timer_stop(txCompleteCheckTimer);
delete currentPayload;
}
Notification::Play(&sequence_blink_start_magenta);
currentPayload = payload;
repeatsLeft = payload->GetRequiredSofwareRepeats() - 1;
startTransmission(frequency);
uint32_t interval = furi_kernel_get_tick_frequency() / 100; // every 10 ms
furi_timer_start(txCompleteCheckTimer, interval);
}
private:
void startTransmission(uint32_t frequency) {
stopTransmission();
if(frequency > 0) {
setFrequencyIgnoringStateChecks(frequency);
}
transmitter = subghz_transmitter_alloc_init(environment, currentPayload->GetProtocol());
subghz_transmitter_deserialize(transmitter, currentPayload->GetFlipperFormat());
subghz_devices_flush_tx(device);
subghz_devices_start_async_tx(device, (void*)subghz_transmitter_yield, transmitter);
}
void stopTransmission() {
if(transmitter != NULL) {
subghz_devices_stop_async_tx(device);
subghz_transmitter_free(transmitter);
transmitter = NULL;
}
}
public:
void StopReceive() {
subghz_worker_stop(worker);
subghz_devices_stop_async_rx(device);
subghz_devices_idle(device);
state = IDLE;
}
void StopTranmit() {
Notification::Play(&sequence_blink_stop);
repeatsLeft = 0;
delete currentPayload;
furi_timer_stop(txCompleteCheckTimer);
stopTransmission();
subghz_devices_idle(device);
state = IDLE;
}
void PutToIdle() {
switch(state) {
case RECEIVING:
StopReceive();
break;
case TRANSMITTING:
StopTranmit();
break;
default:
case IDLE:
break;
}
}
bool IsExternal() {
return isExternal;
}
~SubGhzModule() {
PutToIdle();
if(txCompleteCheckTimer != NULL) {
furi_timer_free(txCompleteCheckTimer);
}
if(furi_hal_power_is_otg_enabled()) {
furi_hal_power_disable_otg();
}
if(worker != NULL) {
subghz_worker_free(worker);
worker = NULL;
}
if(receiver != NULL) {
subghz_receiver_free(receiver);
receiver = NULL;
}
if(environment != NULL) {
subghz_environment_free(environment);
environment = NULL;
}
if(device != NULL) {
subghz_devices_end(device);
subghz_devices_deinit();
device = NULL;
}
}
};

View File

@@ -0,0 +1,65 @@
#pragma once
#include <algorithm>
#include "flipper_format.h"
using namespace std;
#undef LOG_TAG
#define LOG_TAG "SG_PLD"
class SubGhzPayload {
private:
const char* protocol;
FlipperFormat* flipperFormat;
int requiredSoftwareRepeats = 1;
public:
SubGhzPayload(const char* protocol) {
this->protocol = protocol;
flipperFormat = flipper_format_string_alloc();
flipper_format_write_string_cstr(flipperFormat, "Protocol", protocol);
}
void SetKey(uint64_t key) {
char* dataBytes = (char*)&key;
reverse(dataBytes, dataBytes + sizeof(key));
flipper_format_write_hex(flipperFormat, "Key", (const uint8_t*)dataBytes, sizeof(key));
}
void SetBits(uint32_t bits) {
flipper_format_write_uint32(flipperFormat, "Bit", &bits, 1);
}
void SetTE(uint32_t te) {
flipper_format_write_uint32(flipperFormat, "TE", &te, 1);
}
void SetRepeat(uint32_t repeats) {
flipper_format_write_uint32(flipperFormat, "Repeat", &repeats, 1);
}
void SetSoftwareRepeats(uint32_t repeats) {
this->requiredSoftwareRepeats = repeats;
}
FlipperFormat* GetFlipperFormat() {
return flipperFormat;
}
int GetRequiredSofwareRepeats() {
return requiredSoftwareRepeats;
}
const char* GetProtocol() {
return protocol;
}
~SubGhzPayload() {
if(flipperFormat != NULL) {
flipper_format_free(flipperFormat);
flipperFormat = NULL;
}
}
};

View File

@@ -0,0 +1,7 @@
#pragma once
enum SubGhzState {
IDLE,
RECEIVING,
TRANSMITTING,
};

View File

@@ -0,0 +1,12 @@
#pragma once
#include <cstdint>
class SubGhzReceivedData {
public:
virtual const char* GetProtocolName() = 0;
virtual uint32_t GetHash() = 0;
virtual ~SubGhzReceivedData() {};
virtual int GetTE() = 0;
virtual uint32_t GetFrequency() = 0;
};

View File

@@ -0,0 +1,48 @@
#pragma once
#include <lib/subghz/protocols/base.h>
#include "SubGhzReceivedData.hpp"
class SubGhzReceivedDataImpl : public SubGhzReceivedData {
private:
uint32_t frequency;
SubGhzProtocolDecoderBase* decoder;
public:
SubGhzReceivedDataImpl(SubGhzProtocolDecoderBase* decoder, uint32_t frequency) {
this->frequency = frequency;
this->decoder = decoder;
}
const char* GetProtocolName() {
return decoder->protocol->name;
}
uint32_t GetHash() {
//return decoder->protocol->decoder->get_hash_data_long(decoder);
return decoder->protocol->decoder->get_hash_data(decoder);
}
int GetTE() {
FuriString* dataString = furi_string_alloc();
decoder->protocol->decoder->get_string(decoder, dataString);
const char* tePrefix = "Te:";
size_t teStart = furi_string_search_str(dataString, tePrefix, 0);
if(teStart == FURI_STRING_FAILURE) {
return -1;
}
const char* cstr = furi_string_get_cstr(dataString);
const char* startPtr = cstr + teStart + strlen(tePrefix);
int te = strtol(startPtr, NULL, 10);
furi_string_free(dataString);
return te;
}
uint32_t GetFrequency() {
return frequency;
}
};

View File

@@ -0,0 +1,40 @@
#pragma once
#include <lib/subghz/protocols/base.h>
#include "SubGhzReceivedData.hpp"
class SubGhzReceivedDataStub : public SubGhzReceivedData {
private:
const char* protocolName;
uint32_t frequency;
uint32_t hash;
int te;
public:
SubGhzReceivedDataStub(const char* protocolName, uint32_t hash) : SubGhzReceivedDataStub(protocolName, 433920000, hash, 212) {
}
SubGhzReceivedDataStub(const char* protocolName, uint32_t frequency, uint32_t hash, int te) {
this->protocolName = protocolName;
this->frequency = frequency;
this->hash = hash;
this->te = te;
}
const char* GetProtocolName() {
return protocolName;
}
uint32_t GetHash() {
return hash;
}
int GetTE() {
return te;
}
uint32_t GetFrequency() {
return frequency;
}
};

View File

@@ -0,0 +1,139 @@
#pragma once
#include <forward_list>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/modules/loading.h>
#include "view/UiView.hpp"
#undef LOG_TAG
#define LOG_TAG "UI_MGR"
using namespace std;
static void* __ui_manager_instance = NULL;
class UiManager {
private:
Gui* gui = NULL;
ViewDispatcher* viewDispatcher = NULL;
forward_list<UiView*> viewStack;
uint8_t viewStackSize = 0;
uint32_t loadingId = 9999;
Loading* loading = NULL;
UiManager() {
}
static uint32_t backCallback(void*) {
UiManager* uiManager = GetInstance();
UiView* currentView = uiManager->viewStack.front();
if(currentView->GoBack()) {
uiManager->PopView(false);
}
return uiManager->currentViewId();
}
uint32_t currentViewId() {
if(viewStack.empty()) {
return VIEW_NONE;
}
return viewStackSize;
}
void freeLoading() {
if(loading != NULL) {
view_dispatcher_remove_view(viewDispatcher, loadingId);
loading_free(loading);
loading = NULL;
}
}
void showView(uint32_t viewId) {
freeLoading();
view_dispatcher_switch_to_view(viewDispatcher, viewId);
}
public:
void ShowLoading() {
if(loading == NULL) {
loading = loading_alloc();
View* loadingView = loading_get_view(loading);
view_dispatcher_add_view(viewDispatcher, loadingId, loadingView);
view_dispatcher_switch_to_view(viewDispatcher, loadingId);
}
}
static UiManager* GetInstance() {
if(__ui_manager_instance == NULL) {
__ui_manager_instance = new UiManager();
}
return (UiManager*)__ui_manager_instance;
}
void InitGui() {
gui = (Gui*)furi_record_open(RECORD_GUI);
viewDispatcher = view_dispatcher_alloc();
view_dispatcher_attach_to_gui(viewDispatcher, gui, ViewDispatcherTypeFullscreen);
}
void PushView(UiView* view) {
if(!viewStack.empty()) {
viewStack.front()->SetOnTop(false);
}
viewStackSize++;
viewStack.push_front(view);
view->SetOnTop(true);
view_set_previous_callback(view->GetNativeView(), backCallback);
view_dispatcher_add_view(viewDispatcher, currentViewId(), view->GetNativeView());
showView(currentViewId());
}
void PopView(bool preserveView) {
UiView* currentView = viewStack.front();
currentView->SetOnTop(false);
view_dispatcher_remove_view(viewDispatcher, currentViewId());
viewStack.pop_front();
viewStackSize--;
if(!viewStack.empty()) {
UiView* viewReturningTo = viewStack.front();
viewReturningTo->SetOnTop(true);
viewReturningTo->OnReturn();
}
showView(currentViewId());
if(!preserveView) {
delete currentView;
}
}
void RunEventLoop() {
while(!viewStack.empty()) {
view_dispatcher_run(viewDispatcher);
}
}
~UiManager() {
while(!viewStack.empty()) {
PopView(false);
}
freeLoading();
if(viewDispatcher != NULL) {
view_dispatcher_free(viewDispatcher);
viewDispatcher = NULL;
}
if(gui != NULL) {
furi_record_close(RECORD_GUI);
gui = NULL;
}
}
};

View File

@@ -0,0 +1,281 @@
#pragma once
#include <furi.h>
#include <gui/gui.h>
#include <gui/elements.h>
#include "UiView.hpp"
#include "lib/String.hpp"
#undef LOG_TAG
#define LOG_TAG "UI_ADV_SUBMENU"
using namespace std;
// Inspired by https://github.com/flipperdevices/flipperzero-firmware/blob/dev/applications/main/subghz/views/receiver.c
#define FRAME_HEIGHT 12
#define ITEMS_ON_SCREEN 4
class ColumnOrientedListUiView : public UiView {
private:
View* view = NULL;
const char* noElementsCapton = NULL;
const char* leftButtonCaption = NULL;
const char* ceneterButtonCaption = NULL;
const char* rightButtonCaption = NULL;
function<void(int)> leftButtonPress = NULL;
function<void(int)> centerButtonPress = NULL;
function<void(int)> rightButtonPress = NULL;
int listOffset = 0;
int selectedIndex = 0;
int elementsCount = 0;
int8_t columnCount;
int8_t* columnOffsets;
Font* columnFonts = NULL;
Align* columnAlignments = NULL;
function<void(int, int, String* name)> getColumnElementName;
public:
ColumnOrientedListUiView(
int8_t* columnOffsets,
int8_t columnCount,
function<void(int, int, String* name)> columnElementNameGetter
) {
this->columnCount = columnCount;
this->columnOffsets = columnOffsets;
this->getColumnElementName = columnElementNameGetter;
view = view_alloc();
view_set_context(view, this);
view_set_draw_callback(view, drawCallback);
view_set_input_callback(view, inputCallback);
view_set_enter_callback(view, enterCallback);
view_set_exit_callback(view, exitCallback);
view_allocate_model(view, ViewModelTypeLockFree, sizeof(UiVIewPointerViewModel*));
with_view_model_cpp(view, UiVIewPointerViewModel*, model, model->uiVIew = this;, false);
}
void SetNoElementCaption(const char* noElementsCapton) {
this->noElementsCapton = noElementsCapton;
}
void SetColumnFonts(Font* columnFonts) {
this->columnFonts = columnFonts;
}
void SetColumnAlignments(Align* columnAlignments) {
this->columnAlignments = columnAlignments;
}
void SetLeftButton(const char* caption, function<void(int)> pressHandler) {
leftButtonCaption = caption;
leftButtonPress = pressHandler;
}
void SetCenterButton(const char* caption, function<void(int)> pressHandler) {
ceneterButtonCaption = caption;
centerButtonPress = pressHandler;
}
void SetRightButton(const char* caption, function<void(int)> pressHandler) {
rightButtonCaption = caption;
rightButtonPress = pressHandler;
}
void AddElement() {
if(elementsCount == 0 || selectedIndex == elementsCount - 1) {
if(IsOnTop()) {
setIndex(elementsCount);
}
}
elementsCount++;
}
void Refresh() {
view_commit_model(view, true);
}
View* GetNativeView() {
return view;
}
int GetElementsCount() {
return elementsCount;
}
~ColumnOrientedListUiView() {
if(view != NULL) {
OnDestory();
view_free_model(view);
view_free(view);
view = NULL;
}
}
private:
void draw(Canvas* canvas) {
if(!IsOnTop()) {
return;
}
canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
canvas_set_font(canvas, FontSecondary);
if(leftButtonCaption != NULL) elements_button_left(canvas, leftButtonCaption);
if(ceneterButtonCaption != NULL) elements_button_center(canvas, ceneterButtonCaption);
if(rightButtonCaption != NULL) elements_button_right(canvas, rightButtonCaption);
if(elementsCount == 0 && noElementsCapton != NULL) {
int wCenter = canvas_width(canvas) / 2;
int hCenter = canvas_height(canvas) / 2;
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, wCenter, hCenter, AlignCenter, AlignCenter, noElementsCapton);
canvas_set_font(canvas, FontSecondary);
}
String stringBuffer;
bool scrollbar = elementsCount > 4;
for(int i = 0; i < MIN(elementsCount, ITEMS_ON_SCREEN); i++) {
int idx = CLAMP(i + listOffset, elementsCount, 0);
if(selectedIndex == idx) {
canvas_set_color(canvas, ColorBlack);
canvas_draw_box(canvas, 0, 0 + i * FRAME_HEIGHT, scrollbar ? 122 : 127, FRAME_HEIGHT);
canvas_set_color(canvas, ColorWhite);
canvas_draw_dot(canvas, 0, 0 + i * FRAME_HEIGHT);
canvas_draw_dot(canvas, 1, 0 + i * FRAME_HEIGHT);
canvas_draw_dot(canvas, 0, (0 + i * FRAME_HEIGHT) + 1);
canvas_draw_dot(canvas, 0, (0 + i * FRAME_HEIGHT) + 11);
canvas_draw_dot(canvas, scrollbar ? 121 : 126, 0 + i * FRAME_HEIGHT);
canvas_draw_dot(canvas, scrollbar ? 121 : 126, (0 + i * FRAME_HEIGHT) + 11);
} else {
canvas_set_color(canvas, ColorBlack);
}
for(int8_t column = 0; column < columnCount; column++) {
if(columnFonts != NULL) {
canvas_set_font(canvas, columnFonts[column]);
}
int8_t columnOffset = columnOffsets[column];
getColumnElementName(idx, column, &stringBuffer);
// elements_string_fit_width(canvas, stringBuffer.furiString(), maxWidth);
if(columnAlignments == NULL) {
canvas_draw_str(canvas, columnOffset, 9 + i * FRAME_HEIGHT, stringBuffer.cstr());
} else {
canvas_draw_str_aligned(
canvas, columnOffset, 9 + i * FRAME_HEIGHT, columnAlignments[column], AlignBottom, stringBuffer.cstr()
);
}
canvas_set_font(canvas, FontSecondary);
stringBuffer.Reset();
}
}
if(scrollbar) {
elements_scrollbar_pos(canvas, 128, 0, 49, selectedIndex, elementsCount);
}
}
bool input(InputEvent* event) {
switch(event->key) {
case InputKeyUp:
if(event->type == InputTypePress || event->type == InputTypeRepeat) {
if(selectedIndex == 0) {
setIndex(elementsCount - 1);
} else {
setIndex(selectedIndex - 1);
}
return true;
}
break;
case InputKeyDown:
if(event->type == InputTypePress || event->type == InputTypeRepeat) {
if(selectedIndex >= elementsCount - 1) {
setIndex(0);
} else {
setIndex(selectedIndex + 1);
}
return true;
}
break;
case InputKeyLeft:
if(event->type == InputTypePress && leftButtonPress != NULL) {
leftButtonPress(selectedIndex);
return true;
}
break;
case InputKeyOk:
if(event->type == InputTypePress && centerButtonPress != NULL) {
centerButtonPress(selectedIndex);
return true;
}
break;
case InputKeyRight:
if(event->type == InputTypePress && rightButtonPress != NULL) {
rightButtonPress(selectedIndex);
return true;
}
break;
default:
break;
}
return false;
}
void setIndex(int index) {
selectedIndex = index;
int bounds = elementsCount > 3 ? 2 : elementsCount;
if(elementsCount > 3 && selectedIndex >= elementsCount - 1) {
listOffset = selectedIndex - 3;
} else if(listOffset < selectedIndex - bounds) {
listOffset = CLAMP(listOffset + 1, elementsCount - bounds, 0);
} else if(listOffset > selectedIndex - bounds) {
listOffset = CLAMP(selectedIndex - 1, elementsCount - bounds, 0);
}
}
static void drawCallback(Canvas* canvas, void* model) {
ColumnOrientedListUiView* uiView = (ColumnOrientedListUiView*)((UiVIewPointerViewModel*)model)->uiVIew;
uiView->draw(canvas);
}
static bool inputCallback(InputEvent* event, void* context) {
ColumnOrientedListUiView* uiView = (ColumnOrientedListUiView*)context;
if(uiView->input(event)) {
uiView->Refresh();
return true;
}
return false;
}
static void enterCallback(void* context) {
UNUSED(context);
}
static void exitCallback(void* context) {
UNUSED(context);
}
};

View File

@@ -0,0 +1,54 @@
#pragma once
#include <gui/modules/dialog_ex.h>
#include "lib/ui/UiManager.hpp"
#include "UiView.hpp"
class DialogUiView : public UiView {
private:
DialogEx* dialog;
function<void(DialogExResult)> resultHandler;
static void resultCallback(DialogExResult result, void* context) {
DialogUiView* dialog = (DialogUiView*)context;
if(dialog->resultHandler != NULL) {
dialog->resultHandler(result);
}
UiManager::GetInstance()->PopView(false);
}
public:
DialogUiView(const char* header, const char* text) {
dialog = dialog_ex_alloc();
dialog_ex_set_header(dialog, header, 128 / 2, 64 / 4, AlignCenter, AlignCenter);
dialog_ex_set_text(dialog, text, 128 / 2, 64 / 2, AlignCenter, AlignCenter);
}
void AddLeftButton(const char* label) {
dialog_ex_set_left_button_text(dialog, label);
}
void AddRightButton(const char* label) {
dialog_ex_set_right_button_text(dialog, label);
}
void AddCenterButton(const char* label) {
dialog_ex_set_center_button_text(dialog, label);
}
void SetResultHandler(function<void(DialogExResult)> handler) {
resultHandler = handler;
dialog_ex_set_context(dialog, this);
dialog_ex_set_result_callback(dialog, resultCallback);
}
View* GetNativeView() {
return dialog_ex_get_view(dialog);
}
~DialogUiView() {
OnDestory();
dialog_ex_free(dialog);
}
};

View File

@@ -0,0 +1,73 @@
#pragma once
#include "gui/elements.h"
#include <furi.h>
#include <gui/gui.h>
#include <gui/modules/popup.h>
#include "UiView.hpp"
#undef LOG_TAG
#define LOG_TAG "UI_VARITEMLST"
using namespace std;
class ProgressbarPopupUiView : public UiView {
private:
View* view;
const char* header;
const char* progressText;
float progressValue = 0.0f;
public:
ProgressbarPopupUiView(const char* header) {
this->header = header;
view = view_alloc();
view_set_context(view, this);
view_set_draw_callback(view, drawCallback);
view_allocate_model(view, ViewModelTypeLockFree, sizeof(ProgressbarPopupUiView*));
with_view_model_cpp(view, UiVIewPointerViewModel*, model, model->uiVIew = this;, false);
}
void draw(Canvas* canvas) {
canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, 64, 22, AlignCenter, AlignCenter, header);
canvas_set_font(canvas, FontSecondary);
elements_progress_bar_with_text(canvas, 4, 32, 120, progressValue, progressText);
}
void SetProgress(const char* progressText, float progressValue) {
this->progressText = progressText;
this->progressValue = progressValue;
Refresh();
}
void Refresh() {
view_commit_model(view, true);
}
View* GetNativeView() {
return view;
}
~ProgressbarPopupUiView() {
if(view != NULL) {
OnDestory();
view_free_model(view);
view_free(view);
view = NULL;
}
}
private:
static void drawCallback(Canvas* canvas, void* model) {
ProgressbarPopupUiView* uiView = (ProgressbarPopupUiView*)((UiVIewPointerViewModel*)model)->uiVIew;
uiView->draw(canvas);
}
};

View File

@@ -0,0 +1,83 @@
#pragma once
#include <furi.h>
#include <gui/gui.h>
#include <gui/modules/submenu.h>
#include "UiView.hpp"
#include "lib/HandlerContext.hpp"
#include <forward_list>
#undef LOG_TAG
#define LOG_TAG "UI_SUBMENU"
using namespace std;
class SubMenuUiView : public UiView {
private:
Submenu* menu;
uint32_t elementCount = 0;
forward_list<HandlerContext<function<void(uint32_t)>>*> handlers;
static void executeCallback(void* context, uint32_t index) {
if(context == NULL) {
return;
}
HandlerContext<function<void(uint32_t)>>* handlerContext = (HandlerContext<function<void(uint32_t)>>*)context;
handlerContext->GetHandler()(index);
}
public:
SubMenuUiView() {
menu = submenu_alloc();
}
SubMenuUiView(const char* header) : SubMenuUiView() {
SetHeader(header);
}
void SetHeader(const char* header) {
submenu_set_header(menu, header);
}
void AddItem(const char* label, function<void(uint32_t)> handler) {
AddItemAt(elementCount, label, handler);
}
void AddItemAt(uint32_t index, const char* label, function<void(uint32_t)> handler) {
auto handlerContext = new HandlerContext(handler);
handlers.push_front(handlerContext);
submenu_add_item(menu, label, index, executeCallback, handlerContext);
elementCount++;
}
void SetItemLabel(uint32_t index, const char* label) {
submenu_change_item_label(menu, index, label);
}
void SetSelectedItem(uint32_t index) {
submenu_set_selected_item(menu, index);
}
View* GetNativeView() {
return submenu_get_view(menu);
}
uint32_t GetCurrentIndex() {
return submenu_get_selected_item(menu);
}
~SubMenuUiView() {
if(menu != NULL) {
OnDestory();
for(auto handlerContext : handlers) {
delete handlerContext;
}
submenu_free(menu);
menu = NULL;
}
}
};

View File

@@ -0,0 +1,52 @@
#pragma once
#include "lib/String.hpp"
#include <furi.h>
#include <gui/gui.h>
#include <gui/modules/text_input.h>
#include "UiView.hpp"
class TextInputUiView : public UiView {
private:
TextInput* textInput;
char* textBuffer;
size_t bufferSize;
function<void(char*)> inputHandler;
static void inputCallback(void* context) {
TextInputUiView* instance = (TextInputUiView*)context;
if(instance->inputHandler != NULL) {
instance->inputHandler(instance->textBuffer);
}
}
public:
TextInputUiView(const char* header, size_t minLength, size_t maxLength) {
textInput = text_input_alloc();
text_input_set_header_text(textInput, header);
text_input_set_minimum_length(textInput, minLength);
textBuffer = new char[maxLength];
bufferSize = maxLength;
}
void SetDefaultText(String* text) {
strcpy(textBuffer, text->cstr());
}
void SetResultHandler(function<void(char*)> handler) {
inputHandler = handler;
text_input_set_result_callback(textInput, inputCallback, this, textBuffer, bufferSize, false);
}
View* GetNativeView() {
return text_input_get_view(textInput);
}
~TextInputUiView() {
OnDestory();
text_input_free(textInput);
delete[] textBuffer;
}
};

View File

@@ -0,0 +1,66 @@
#pragma once
#include <functional>
#include <gui/gui.h>
#include <gui/view.h>
using namespace std;
class UiView {
private:
function<void()> onDestroyHandler = NULL;
function<void()> onReturnToView = NULL;
function<bool()> goBackHandler = NULL;
bool isOnTop = false;
public:
virtual View* GetNativeView() = 0;
virtual ~UiView() {
}
void SetOnDestroyHandler(function<void()> handler) {
onDestroyHandler = handler;
}
void SetOnReturnToViewHandler(function<void()> handler) {
onReturnToView = handler;
}
void SetGoBackHandler(function<bool()> handler) {
goBackHandler = handler;
}
void OnReturn() {
if(onReturnToView != NULL) {
onReturnToView();
}
}
bool IsOnTop() {
return isOnTop;
}
void SetOnTop(bool value) {
this->isOnTop = value;
}
bool GoBack() {
if(goBackHandler != NULL) {
return goBackHandler();
}
return true;
}
protected:
// Must be called from parent class destructor's!
void OnDestory() {
if(onDestroyHandler != NULL) {
onDestroyHandler();
}
}
};
struct UiVIewPointerViewModel {
UiView* uiVIew;
};

View File

@@ -0,0 +1,53 @@
#pragma once
#include <furi.h>
#include <gui/gui.h>
#include <gui/modules/variable_item_list.h>
#include "UiView.hpp"
#include "item/UiVariableItem.hpp"
#undef LOG_TAG
#define LOG_TAG "UI_VARITEMLST"
using namespace std;
class VariableItemListUiView : public UiView {
private:
uint32_t itemCounter = 0;
VariableItemList* varItemList;
function<void(uint32_t)> enterPressHandler;
static void onEnterCallback(void* context, uint32_t index) {
VariableItemListUiView* view = (VariableItemListUiView*)context;
view->enterPressHandler(index);
}
public:
VariableItemListUiView() {
varItemList = variable_item_list_alloc();
}
uint32_t AddItem(UiVariableItem* item) {
item->AddTo(varItemList);
return itemCounter++;
}
void SetEnterPressHandler(function<void(uint32_t)> handler) {
enterPressHandler = handler;
variable_item_list_set_enter_callback(varItemList, onEnterCallback, this);
}
View* GetNativeView() {
return variable_item_list_get_view(varItemList);
}
~VariableItemListUiView() {
if(varItemList != NULL) {
OnDestory();
variable_item_list_free(varItemList);
varItemList = NULL;
}
}
};

View File

@@ -0,0 +1,68 @@
#pragma once
#include <functional>
#include "gui/modules/variable_item_list.h"
using namespace std;
class UiVariableItem {
private:
VariableItem* item = NULL;
const char* label;
uint8_t selectedIndex;
uint8_t valuesCount;
function<const char*(uint8_t)> changeHandler;
static void itemChangeCallback(VariableItem* item) {
UiVariableItem* uiItem = (UiVariableItem*)variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, uiItem->changeHandler(index));
}
public:
UiVariableItem(const char* label, const char* staticValue) :
UiVariableItem(label, [staticValue](uint8_t) { return staticValue; }) {
}
UiVariableItem(const char* label, function<const char*(uint8_t)> changeHandler) : UiVariableItem(label, 0, 1, changeHandler) {
}
UiVariableItem(const char* label, uint8_t selectedIndex, uint8_t valuesCount, function<const char*(uint8_t)> changeHandler) {
this->label = label;
this->selectedIndex = selectedIndex;
this->valuesCount = valuesCount;
this->changeHandler = changeHandler;
}
void AddTo(VariableItemList* varItemList) {
item = variable_item_list_add(varItemList, label, valuesCount, itemChangeCallback, this);
Refresh();
}
void SetSelectedItem(uint8_t selectedIndex, uint8_t valuesCount) {
this->selectedIndex = selectedIndex;
this->valuesCount = valuesCount;
Refresh();
}
void Refresh() {
if(item == NULL) {
return;
}
variable_item_set_values_count(item, valuesCount);
variable_item_set_current_value_index(item, selectedIndex);
itemChangeCallback(item);
}
bool Editable() {
return valuesCount > 1;
}
~UiVariableItem() {
}
};

View File

@@ -0,0 +1,6 @@
{
"label": "Build, Clear & Launch",
"group": "build",
"type": "shell",
"command": "python scripts/build-and-clear.py"
},

View File

@@ -0,0 +1,33 @@
import os
import lief
import pathlib
UFBT_PATH = pathlib.Path.home() / ".ufbt"
FAP_LOCATION_AFTER_BUILD = "dist/chief_cooker.fap"
FAP_LOCATION_ON_FLIPPER = "/ext/apps/Sub-GHz/chief_cooker.fap"
OBJCOPY_PATH = UFBT_PATH / "toolchain/current/bin/arm-none-eabi-objcopy"
def clearSections():
binary = lief.parse(FAP_LOCATION_AFTER_BUILD)
for i, sect in enumerate(binary.sections):
if sect.name.find("_Z") >= 0:
newSectName = "_s%d" % i
cmd = '%s "%s" --rename-section %s=%s' % (
OBJCOPY_PATH,
FAP_LOCATION_AFTER_BUILD,
sect.name,
newSectName,
)
print("Renaming to %s from %s" % (newSectName, sect.name))
os.system(cmd)
os.system("ufbt")
clearSections()
os.system(
"%s/toolchain/current/python/python %s/current/scripts/runfap.py -p auto -s %s -t %s"
% (UFBT_PATH, UFBT_PATH, FAP_LOCATION_AFTER_BUILD, FAP_LOCATION_ON_FLIPPER)
)

View File

@@ -0,0 +1,11 @@
8028a0 = 1000 0000 0010 1000 1010 0000
84e8a0 = 1000 0100 1110 1000 1010 0000
e6f810 = 1110 0110 1111 1000 0001 0000
e6b810 = 1110 0110 1011 1000 0001 0000
From https://dev.xcjs.com/r0073dl053r/flipper-playground/-/tree/main/Sub-GHz/Restaurant_Pagers/Retekess_Pagers/T111?ref_type=heads
Pager1 = A21080 = 1010 0010 0001 0000 1000 0000
Pager2 = A21040 = 1010 0010 0001 0000 0100 0000
last 8 bits reversed seems to be number
will name it L8R - Last 8 bits Reversed

View File

@@ -0,0 +1,12 @@
.vscode
.clang-format
.clangd
.editorconfig
.env
.ufbt
# Ignore the makefile
Makefile
#ignore folder
/dist

View File

@@ -0,0 +1,150 @@
# CAN Commander DBC Decoding Profile Format (`.dbcprof`)
## Purpose
`.dbcprof` is a CAN Commander custom profile format for storing a curated DBC subset on Flipper.
Each file stores:
- up to 16 signal definitions
- optional value mapping per signal (`raw -> label`) up to 16 entries per signal
This is not a raw `.dbc` file. It is an app-native profile format.
## Storage Location
- Directory: `apps_data/can_commander/dbc_profiles`
- Extension: `.dbcprof`
## File Header
Every file must start with:
```text
Filetype: CANCommanderDbcProfile
Version: 1
```
If filetype/version do not match, load is rejected.
## Top-Level Keys
- `name`: user-facing profile name
- `signal_count`: number of defined signals in this file (0..16)
## Per-Signal Keys
Signals are 1-indexed: `signal1_*`, `signal2_*`, ... `signalN_*`.
Required/expected keys for each signal:
- `signalX_sid`: signal id (`0..65535`, decimal)
- `signalX_name`: user-facing signal name shown in decode UI (text, optional but recommended)
- `signalX_bus`: `can0`, `can1`, or `both`
- `signalX_id`: CAN ID in hex (no `0x` prefix required)
- `signalX_ext`: `0` or `1` (11-bit vs 29-bit ID)
- `signalX_start`: start bit (`0..63`)
- `signalX_len`: bit length (`1..64`)
- `signalX_order`: `intel` or `motorola`
- `signalX_sign`: `u` (unsigned) or `s` (signed)
- `signalX_factor`: float scaling factor
- `signalX_offset`: float offset
- `signalX_min`: float minimum decoded range
- `signalX_max`: float maximum decoded range
- `signalX_unit`: unit text (short string, can be empty)
## Value Mapping Keys
Optional per signal.
- `signalX_map_count`: number of mappings for this signal (`0..16`)
- For each mapping index `Y`:
- `signalX_mapY_raw`: integer raw value (signed 64-bit supported)
- `signalX_mapY_label`: display label (text)
Example meaning:
- `raw=0 -> "Off"`
- `raw=1 -> "On"`
## Runtime Behavior
- **Save source**: app local DBC cache (not firmware list export parsing).
- **Load behavior**: replace-all in firmware (`dbc clear`, then add all signals from file).
- **Decode UI mapping**:
- if `sid/raw` has a map entry, UI shows mapped label as primary
- raw/numeric value remains shown as secondary context
## Limits and Validation
- Max signals per file: `16`
- Max value maps per signal: `16`
- On malformed fields, missing required signal fields, or invalid header/version:
- load fails safely
- no crash
## Backward Compatibility
- Legacy `.dcfg` files are still loadable.
- Legacy folder `apps_data/can_commander/dbc_configs/` is still scanned.
- Legacy filetype `CANCommanderDbcConfig` is still accepted.
## Example 1: Single Boolean Signal (Off/On)
```text
Filetype: CANCommanderDbcProfile
Version: 1
name: Door_Status
signal_count: 1
signal1_sid: 100
signal1_name: Door
signal1_bus: can0
signal1_id: 1F2
signal1_ext: 0
signal1_start: 12
signal1_len: 1
signal1_order: intel
signal1_sign: u
signal1_factor: 1
signal1_offset: 0
signal1_min: 0
signal1_max: 1
signal1_unit:
signal1_map_count: 2
signal1_map1_raw: 0
signal1_map1_label: Off
signal1_map2_raw: 1
signal1_map2_label: On
```
## Example 2: Multi-Signal Config
```text
Filetype: CANCommanderDbcProfile
Version: 1
name: Powertrain_Core
signal_count: 2
signal1_sid: 10
signal1_bus: can0
signal1_id: 7E8
signal1_ext: 0
signal1_start: 24
signal1_len: 16
signal1_order: intel
signal1_sign: u
signal1_factor: 0.25
signal1_offset: 0
signal1_min: 0
signal1_max: 16383.75
signal1_unit: rpm
signal1_map_count: 0
signal2_sid: 11
signal2_name: Gear
signal2_bus: can0
signal2_id: 1D5
signal2_ext: 0
signal2_start: 20
signal2_len: 3
signal2_order: intel
signal2_sign: u
signal2_factor: 1
signal2_offset: 0
signal2_min: 0
signal2_max: 7
signal2_unit:
signal2_map_count: 4
signal2_map1_raw: 0
signal2_map1_label: Park
signal2_map2_raw: 1
signal2_map2_label: Reverse
signal2_map3_raw: 2
signal2_map3_label: Neutral
signal2_map4_raw: 3
signal2_map4_label: Drive
```

View File

@@ -0,0 +1,215 @@
# CAN Commander Smart Injection Profile Format (`.injprof`)
This document describes the on-disk format used by the Flipper app to save and load Smart Injection Profiles.
## What a Smart Injection profile is
A Smart Injection profile is a saved preset containing:
- A human-readable set name.
- All 5 Smart Injection slot configurations.
- The currently selected slot index.
This is Flipper-side UI/config persistence. Firmware runtime state is separate.
## File location and naming
Saved Smart Injection profiles are stored in:
`apps_data/can_commander/injection_profiles/`
Each profile is saved as:
`<safe_name>.injprof`
Where `safe_name` is generated from the entered set name:
- letters/digits are kept (letters lowercased)
- spaces, `_`, `-`, `.` become `_`
- repeated separators collapse to one `_`
- trailing `_` is removed
- if empty after sanitization, fallback name is `profile`
Examples:
- `My Horn Set` -> `my_horn_set.injprof`
- `Seat-Heat.v1` -> `seat_heat_v1.injprof`
## Container format
Files use FlipperFormat with this header:
- `Filetype: CANCommanderInjectionProfile`
- `Version: 1`
If header type/version does not match, load fails.
## Top-level keys
- `name` (string): display name for the Smart Injection profile.
- `active_slot` (uint32): selected slot index, `0..4`.
## Per-slot keys
For each slot `N` in `1..5`, the following string keys are stored:
- `slotN_name`
- `slotN_used`
- `slotN_bus`
- `slotN_id`
- `slotN_ext`
- `slotN_mask`
- `slotN_value`
- `slotN_xor`
- `slotN_mux`
- `slotN_mux_start`
- `slotN_mux_len`
- `slotN_mux_value`
- `slotN_sig`
- `slotN_sig_start`
- `slotN_sig_len`
- `slotN_sig_value`
- `slotN_count`
- `slotN_interval_ms`
Example: for slot 3, keys are `slot3_name`, `slot3_used`, etc.
## Field meanings
- `slotN_name`: UI name shown for the slot (for example `Horn`).
- `slotN_used`: `0/1` flag used by UI logic to indicate configured/active slot profile.
- `slotN_bus`: target bus (`can0`, `can1`, or `both`).
- `slotN_id`: CAN ID hex token used for tracking/injection source frame.
- `slotN_ext`: extended-ID flag (`0` standard 11-bit, `1` extended 29-bit).
- `slotN_mask`: 64-bit mask (hex string) for byte-level masked injection.
- `slotN_value`: 64-bit value (hex string) applied with `mask`.
- `slotN_xor`: 64-bit xor mask (hex string) applied after masked set.
- `slotN_mux`: mux filter enabled (`0/1`).
- `slotN_mux_start`: mux field start bit.
- `slotN_mux_len`: mux field length in bits.
- `slotN_mux_value`: mux field compare value.
- `slotN_sig`: signal/field injection enabled (`0/1`).
- `slotN_sig_start`: signal field start bit.
- `slotN_sig_len`: signal field length in bits.
- `slotN_sig_value`: signal field value.
- `slotN_count`: inject repeat count for this slot preset.
- `slotN_interval_ms`: interval between repeats for this slot preset.
## Defaults and backward compatibility
When loading:
- Missing per-slot keys are filled with defaults.
- Missing or invalid `active_slot` falls back to slot index `0`.
- Defaults are also used when any loaded token is empty.
Default slot values are effectively:
- `name=SlotN`, `used=0`, `bus=can0`, `id=000`, `ext=0`
- `mask=value=xor=0000000000000000`
- `mux=0`, `mux_start=0`, `mux_len=1`, `mux_value=0`
- `sig=0`, `sig_start=0`, `sig_len=1`, `sig_value=0`
- `count=1`, `interval_ms=0`
## Example file
```ini
Filetype: CANCommanderInjectionProfile
Version: 1
name: track_day
slot1_name: Horn
slot1_used: 1
slot1_bus: can0
slot1_id: 1D5
slot1_ext: 0
slot1_mask: 0000000000000000
slot1_value: 0000000000000000
slot1_xor: 0000000000000000
slot1_mux: 0
slot1_mux_start: 0
slot1_mux_len: 1
slot1_mux_value: 0
slot1_sig: 1
slot1_sig_start: 20
slot1_sig_len: 3
slot1_sig_value: 5
slot1_count: 5
slot1_interval_ms: 100
slot2_name: Slot2
slot2_used: 0
slot2_bus: can0
slot2_id: 000
slot2_ext: 0
slot2_mask: 0000000000000000
slot2_value: 0000000000000000
slot2_xor: 0000000000000000
slot2_mux: 0
slot2_mux_start: 0
slot2_mux_len: 1
slot2_mux_value: 0
slot2_sig: 0
slot2_sig_start: 0
slot2_sig_len: 1
slot2_sig_value: 0
slot2_count: 1
slot2_interval_ms: 0
slot3_name: Slot3
slot3_used: 0
slot3_bus: can0
slot3_id: 000
slot3_ext: 0
slot3_mask: 0000000000000000
slot3_value: 0000000000000000
slot3_xor: 0000000000000000
slot3_mux: 0
slot3_mux_start: 0
slot3_mux_len: 1
slot3_mux_value: 0
slot3_sig: 0
slot3_sig_start: 0
slot3_sig_len: 1
slot3_sig_value: 0
slot3_count: 1
slot3_interval_ms: 0
slot4_name: Slot4
slot4_used: 0
slot4_bus: can0
slot4_id: 000
slot4_ext: 0
slot4_mask: 0000000000000000
slot4_value: 0000000000000000
slot4_xor: 0000000000000000
slot4_mux: 0
slot4_mux_start: 0
slot4_mux_len: 1
slot4_mux_value: 0
slot4_sig: 0
slot4_sig_start: 0
slot4_sig_len: 1
slot4_sig_value: 0
slot4_count: 1
slot4_interval_ms: 0
slot5_name: Slot5
slot5_used: 0
slot5_bus: can0
slot5_id: 000
slot5_ext: 0
slot5_mask: 0000000000000000
slot5_value: 0000000000000000
slot5_xor: 0000000000000000
slot5_mux: 0
slot5_mux_start: 0
slot5_mux_len: 1
slot5_mux_value: 0
slot5_sig: 0
slot5_sig_start: 0
slot5_sig_len: 1
slot5_sig_value: 0
slot5_count: 1
slot5_interval_ms: 0
active_slot: 0
```
## Notes
- Backward compatibility:
- Legacy `.cfg` files are still loadable.
- Legacy folder `apps_data/can_commander/slot_sets/` is still scanned.
- Legacy filetype `CANCommanderSlotSet` is still accepted.
- Editing these files manually is supported, but malformed values may be clamped/fallback-filled when loaded.
- The app rewrites normalized slot args on load/save, so formatting/order may change.

View File

@@ -0,0 +1,44 @@
App(
appid="can_commander",
name="CAN Commander",
apptype=FlipperAppType.EXTERNAL,
entry_point="cancommander_main",
requires=["gui"],
stack_size=14 * 1024,
fap_icon="icon.png",
fap_category="GPIO",
sources=[
"can_commander.c",
"libraries/can_commander_uart.c",
"views/dashboard/dashboard_core.c",
"views/dashboard/dashboard_read.c",
"views/dashboard/dashboard_bittrack.c",
"views/dashboard/dashboard_metric.c",
"scenes_config/scene_functions.c",
"scenes/main_menu.c",
"scenes/debug_menu.c",
"scenes/tools_menu.c",
"scenes/tools_monitor_menu.c",
"scenes/tools_control_menu.c",
"scenes/tools_vehicle_diag_menu.c",
"scenes/tools_dbc_menu.c",
"scenes/profiles_menu.c",
"scenes/profiles_smart_inject_menu.c",
"scenes/custom_inject_menu.c",
"scenes/custom_inject_slot_menu.c",
"scenes/custom_inject_bytes_menu.c",
"scenes/custom_inject_save_slots_menu.c",
"scenes/custom_inject_load_slots_menu.c",
"scenes/obd_pid_menu.c",
"scenes/obd_pid_list_menu.c",
"scenes/bus_cfg_menu.c",
"scenes/bus_filter_menu.c",
"scenes/dbc_menu.c",
"scenes/dbc_save_config_menu.c",
"scenes/dbc_load_config_menu.c",
"scenes/monitor_scene.c",
"scenes/args_editor_scene.c",
"scenes/text_input_scene.c",
"scenes/status_scene.c",
],
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,293 @@
#pragma once
#include <furi.h>
#include <gui/gui.h>
#include <gui/modules/submenu.h>
#include <gui/modules/byte_input.h>
#include <gui/modules/text_box.h>
#include <gui/modules/text_input.h>
#include <gui/modules/variable_item_list.h>
#include <gui/modules/widget.h>
#include <gui/view.h>
#include <gui/scene_manager.h>
#include <gui/view_dispatcher.h>
#include "libraries/can_commander_uart.h"
#include "scenes_config/scene_functions.h"
#define PROGRAM_VERSION "v2.1.1"
#define APP_DBC_CFG_MAX_SIGNALS 16U
#define APP_DBC_CFG_MAX_MAPS 16U
#define APP_DBC_CFG_LABEL_MAX 16U
#define APP_DBC_CFG_SIGNAL_NAME_MAX 18U
#define APP_CUSTOM_INJECT_SLOT_ARGS_MAX 320U
#define APP_SMART_INJECT_PROFILE_EXT ".injprof"
#define APP_SMART_INJECT_PROFILE_LEGACY_EXT ".cfg"
#define APP_DBC_DECODE_PROFILE_EXT ".dbcprof"
#define APP_DBC_DECODE_PROFILE_LEGACY_EXT ".dcfg"
#define APP_SMART_INJECT_PROFILE_DIR APP_DATA_PATH("injection_profiles")
#define APP_SMART_INJECT_PROFILE_LEGACY_DIR APP_DATA_PATH("slot_sets")
#define APP_DBC_DECODE_PROFILE_DIR APP_DATA_PATH("dbc_profiles")
#define APP_DBC_DECODE_PROFILE_LEGACY_DIR APP_DATA_PATH("dbc_configs")
#define APP_SMART_INJECT_PROFILE_FILETYPE "CANCommanderInjectionProfile"
#define APP_SMART_INJECT_PROFILE_LEGACY_FILETYPE "CANCommanderSlotSet"
#define APP_DBC_DECODE_PROFILE_FILETYPE "CANCommanderDbcProfile"
#define APP_DBC_DECODE_PROFILE_LEGACY_FILETYPE "CANCommanderDbcConfig"
#define APP_SMART_INJECT_PROFILE_VER 1U
#define APP_DBC_DECODE_PROFILE_VER 1U
typedef enum {
AppViewSubmenu = 0,
AppViewTextBox,
AppViewTextInput,
AppViewByteInput,
AppViewVarList,
AppViewWidget,
AppViewDashboard,
} AppView;
typedef enum {
AppCustomEventMonitorUpdated = 0x1000,
AppCustomEventInputDone = 0x1001,
} AppCustomEvent;
typedef enum {
AppDashboardNone = 0,
AppDashboardReadAll,
AppDashboardFiltered,
AppDashboardWrite,
AppDashboardSpeed,
AppDashboardValtrack,
AppDashboardUniqueIds,
AppDashboardBittrack,
AppDashboardReverse,
AppDashboardObdPid,
AppDashboardDbcDecode,
AppDashboardCustomInject,
} AppDashboardMode;
typedef enum {
AppHexInputNone = 0,
AppHexInputU8,
AppHexInputU16,
AppHexInputU32,
AppHexInputBytes,
} AppHexInputMode;
#define APP_ARGS_EDITOR_MAX_ITEMS 16U
#define APP_ARGS_EDITOR_KEY_MAX 24U
#define APP_ARGS_EDITOR_VALUE_MAX 80U
typedef enum {
AppArgValueText = 0,
AppArgValueAction,
AppArgValueBus,
AppArgValueBool01,
AppArgValueOrder,
AppArgValueSign,
AppArgValueModeListen,
AppArgValueModeReverse,
} AppArgValueType;
typedef struct App App;
typedef void (*AppArgsApplyCallback)(App* app);
typedef struct {
bool used;
int64_t raw;
char label[APP_DBC_CFG_LABEL_MAX];
} AppDbcValueMap;
typedef struct {
bool used;
CcDbcSignalDef def;
char signal_name[APP_DBC_CFG_SIGNAL_NAME_MAX];
uint8_t map_count;
AppDbcValueMap maps[APP_DBC_CFG_MAX_MAPS];
} AppDbcSignalCache;
typedef struct {
char key[APP_ARGS_EDITOR_KEY_MAX];
char value[APP_ARGS_EDITOR_VALUE_MAX];
AppArgValueType type;
} AppArgItem;
struct App {
SceneManager* scene_manager;
ViewDispatcher* view_dispatcher;
Submenu* submenu;
TextBox* text_box;
TextInput* text_input;
ByteInput* byte_input;
VariableItemList* var_list;
Widget* widget;
View* dashboard_view;
FuriMutex* mutex;
FuriThread* rx_worker;
bool rx_worker_stop;
bool app_ready;
CcClient* client;
bool connected;
bool tool_active;
bool monitor_scene_active;
bool monitor_update_pending;
uint32_t monitor_last_update_ms;
AppDashboardMode dashboard_mode;
FuriString* monitor_text;
FuriString* status_text;
char input_work[220];
char* input_dest;
size_t input_dest_size;
const char* input_header;
char args_read_all[96];
char args_filtered[128];
char args_write_tool[128];
char args_speed[64];
char args_valtrack[64];
char args_unique_ids[64];
char args_bittrack[96];
char args_reverse_auto[128];
char args_reverse_read[128];
char args_obd_pid[96];
char args_dbc_decode[64];
char args_custom_inject_start[64];
char args_custom_inject_slots[5][APP_CUSTOM_INJECT_SLOT_ARGS_MAX];
char args_custom_inject_bit[64];
char args_custom_inject_clearbit[48];
char args_custom_inject_field[80];
char args_tool_config[96];
char args_bus_cfg_can0[96];
char args_bus_cfg_can1[96];
char args_filter_can0[96];
char args_filter_can1[96];
char args_dbc_add[220];
char args_dbc_remove[48];
char* args_editor_target;
size_t args_editor_target_size;
const char* args_editor_title;
const char* args_editor_apply_label;
AppArgsApplyCallback args_editor_apply_callback;
CanCommanderScene args_editor_apply_next_scene;
bool args_editor_apply_enabled;
uint8_t args_editor_selected_index;
uint8_t args_editor_count;
AppArgItem args_editor_items[APP_ARGS_EDITOR_MAX_ITEMS];
VariableItem* args_editor_var_items[APP_ARGS_EDITOR_MAX_ITEMS];
bool input_editing_arg_value;
uint8_t input_arg_value_index;
bool input_use_byte_input;
AppHexInputMode input_hex_mode;
uint8_t input_hex_store[16];
uint8_t input_hex_count;
uint8_t custom_inject_active_slot;
bool custom_inject_slot_provisioned[5];
char custom_inject_edit_bus[32];
char custom_inject_edit_name[40];
char custom_inject_edit_id[40];
char custom_inject_edit_count[40];
char custom_inject_edit_interval[48];
char custom_inject_edit_bit[48];
char custom_inject_edit_field[64];
char custom_inject_edit_bytes[32];
char custom_inject_edit_value_hex[20];
char custom_inject_edit_mux[96];
char custom_inject_edit_mux_start[24];
char custom_inject_edit_mux_len[24];
char custom_inject_edit_mux_value[24];
char custom_inject_set_name[32];
char dbc_config_name[32];
char dbc_config_save_name[32];
AppDbcSignalCache dbc_config_signals[APP_DBC_CFG_MAX_SIGNALS];
uint8_t dbc_config_signal_count;
CcToolId pending_tool_start_id;
char pending_tool_start_name[24];
};
void app_set_status(App* app, const char* fmt, ...);
void app_append_monitor(App* app, const char* fmt, ...);
void app_refresh_monitor_view(App* app);
void app_refresh_live_view(App* app);
bool app_monitor_uses_dashboard(App* app);
bool app_args_set_key_value(char* args, size_t args_size, const char* key, const char* value);
bool app_connect(App* app, bool force_reconnect);
bool app_require_connected(App* app);
void app_begin_edit(App* app, char* destination, size_t destination_size, const char* header_text);
void app_apply_edit(App* app);
void app_begin_args_editor(
App* app,
char* destination,
size_t destination_size,
const char* header_text);
void app_begin_args_editor_apply(
App* app,
char* destination,
size_t destination_size,
const char* header_text,
const char* apply_label,
AppArgsApplyCallback apply_callback,
CanCommanderScene next_scene);
void app_action_ping(App* app);
void app_action_get_info(App* app);
void app_action_stats(App* app);
void app_action_start_read_all(App* app);
void app_action_tool_start(App* app, CcToolId tool_id, const char* args, const char* label);
void app_action_tool_config(App* app, const char* args);
void app_action_tool_stop(App* app);
void app_action_tool_status(App* app);
void app_action_custom_inject_add(App* app, uint8_t slot_number);
void app_action_custom_inject_modify(App* app, uint8_t slot_number);
void app_action_custom_inject_remove(App* app, uint8_t slot_number);
void app_action_custom_inject_clear(App* app);
void app_action_custom_inject_list(App* app);
void app_action_custom_inject_cancel(App* app);
void app_action_custom_inject_bit(App* app);
void app_action_custom_inject_clearbit(App* app);
void app_action_custom_inject_field(App* app);
void app_action_custom_inject_inject(App* app, uint8_t slot_number);
void app_action_custom_inject_sync_slots(App* app);
void app_custom_inject_set_active_slot(App* app, uint8_t slot_index);
uint8_t app_custom_inject_get_active_slot(const App* app);
char* app_custom_inject_get_slot_args(App* app, uint8_t slot_index);
void app_custom_inject_reset_slot(App* app, uint8_t slot_index);
void app_custom_inject_reset_all_slots(App* app);
void app_custom_inject_load(App* app);
void app_custom_inject_save(App* app);
bool app_custom_inject_save_slot_set(App* app, const char* set_name);
bool app_custom_inject_load_slot_set_file(App* app, const char* file_path);
void app_action_bus_set_cfg(App* app, CcBus bus, const char* args);
void app_action_bus_get_cfg(App* app, CcBus bus);
void app_action_bus_set_filter(App* app, CcBus bus, const char* args);
void app_action_bus_clear_filter(App* app, CcBus bus);
void app_action_dbc_clear(App* app);
void app_action_dbc_add(App* app, const char* args);
void app_action_dbc_remove(App* app, const char* args);
void app_action_dbc_list(App* app);
void app_dbc_config_reset(App* app);
bool app_dbc_config_save_file(App* app, const char* config_name);
bool app_dbc_config_load_file(App* app, const char* file_path, bool apply_to_firmware);
const char* app_dbc_config_lookup_label(const App* app, uint16_t sid, int64_t raw);
const char* app_dbc_config_lookup_signal_name(const App* app, uint16_t sid);
int32_t cancommander_main(void* p);

Some files were not shown because too many files have changed in this diff Show More