V38 Release candidate changes (#79)

This commit is contained in:
Clara K
2023-01-11 19:13:29 +01:00
committed by GitHub
2679 changed files with 9332 additions and 12251 deletions
-311
View File
@@ -1,311 +0,0 @@
kind: pipeline
type: docker
name: "Release firmware"
steps:
- name: "Update submodules"
image: alpine/git
commands:
- git submodule sync
- git -c protocol.version=2 submodule update --init --force --recursive
- git submodule foreach git config --local gc.auto 0
- git log -1 --format='%H'
- name: "Build firmware"
image: hfdj/fztools
pull: never
commands:
- export DIST_SUFFIX=${DRONE_TAG}
- export WORKFLOW_BRANCH_OR_TAG=release-cfw
- ./fbt COMPACT=1 DEBUG=0 updater_package
- mkdir artifacts-default
- mv dist/f7-C/* artifacts-default/
- ls -laS artifacts-default
- ls -laS artifacts-default/f7-update-${DRONE_TAG}
- sed -i 's/(version)/'${DRONE_TAG}'/g' CHANGELOG.md
- echo '# [Install via Web Updater](https://lab.flipper.net/?url=https://unleashedflip.com/fw/${DRONE_TAG}/flipper-z-f7-update-'${DRONE_TAG}'.tgz&channel=release-cfw&version='${DRONE_TAG}')' >> CHANGELOG.md
environment:
FBT_TOOLS_CUSTOM_LINK:
from_secret: fbt_link
- name: "Build no anims FW"
image: hfdj/fztools
pull: never
commands:
- rm -f assets/dolphin/external/manifest.txt
- cp .ci_files/anims_ofw.txt assets/dolphin/external/manifest.txt
- export DIST_SUFFIX=${DRONE_TAG}n
- export WORKFLOW_BRANCH_OR_TAG=no-custom-anims
- ./fbt COMPACT=1 DEBUG=0 updater_package
- mkdir artifacts-ofw-anims
- mv dist/f7-C/* artifacts-ofw-anims/
- ls -laS artifacts-ofw-anims
- ls -laS artifacts-ofw-anims/f7-update-${DRONE_TAG}n
- echo '' >> CHANGELOG.md
- echo '### [Version without custom animations - Install via Web Updater](https://lab.flipper.net/?url=https://unleashedflip.com/fw_no_anim/flipper-z-f7-update-'${DRONE_TAG}'n.tgz&channel=release-cfw&version='${DRONE_TAG}'n)' >> CHANGELOG.md
environment:
FBT_TOOLS_CUSTOM_LINK:
from_secret: fbt_link
- name: "Bundle self-update packages"
image: kramos/alpine-zip
commands:
- cp artifacts-ofw-anims/flipper-z-f7-update-${DRONE_TAG}n.tgz .
- cp artifacts-default/flipper-z-f7-update-${DRONE_TAG}.tgz .
- zip -r artifacts-ofw-anims/flipper-z-f7-update-${DRONE_TAG}n.zip artifacts-ofw-anims/f7-update-${DRONE_TAG}n
- zip -r artifacts-default/flipper-z-f7-update-${DRONE_TAG}.zip artifacts-default/f7-update-${DRONE_TAG}
- tar czpf artifacts-default/flipper-z-any-scripts-${DRONE_TAG}.tgz scripts debug
- rm -rf artifacts-ofw-anims/f7-update-${DRONE_TAG}
- rm -rf artifacts-default/f7-update-${DRONE_TAG}
- ls -laS artifacts-ofw-anims
- ls -laS artifacts-default
- mv artifacts-default/ ${DRONE_TAG}
- ls -laS ${DRONE_TAG}
- name: "Upload default to updates srv"
image: appleboy/drone-scp
settings:
host:
from_secret: dep_host
username:
from_secret: dep_user
password:
from_secret: dep_passwd
port:
from_secret: dep_port
target:
from_secret: dep_target_new
source:
- ${DRONE_TAG}/*.tgz
- ${DRONE_TAG}/*.zip
- ${DRONE_TAG}/*.json
- ${DRONE_TAG}/*.elf
- ${DRONE_TAG}/*.dfu
- ${DRONE_TAG}/*.bin
- name: "Upload no-anims to updates srv"
image: appleboy/drone-scp
settings:
host:
from_secret: dep_host
username:
from_secret: dep_user
password:
from_secret: dep_passwd
port:
from_secret: dep_port
target:
from_secret: dep_target_noanim
source: flipper-z-f7-update-${DRONE_TAG}n.tgz
- name: "Do Github release"
image: ddplugins/github-release
pull: never
settings:
github_url: https://github.com
repo_owner:
from_secret: github_repoowner
api_key:
from_secret: github_apikey
files:
- ${DRONE_TAG}/*.tgz
- ${DRONE_TAG}/*.zip
- artifacts-ofw-anims/*.tgz
title: ${DRONE_TAG}
note: CHANGELOG.md
checksum:
- md5
- sha1
- crc32
- name: "Trigger update server reindex"
image: hfdj/fztools
pull: never
environment:
UPD_KEY:
from_secret: git_update_serv_token
UPD_URL:
from_secret: git_update_server_url
commands:
- curl -X POST -F 'key='$UPD_KEY'' $UPD_URL
- name: "Send files to telegram"
image: appleboy/drone-telegram
settings:
token:
from_secret: tgtoken
to:
from_secret: tgid
format: markdown
message: "New Unleashed firmware released!
Version: {{build.tag}}
[-Github-](https://github.com/DarkFlippers/unleashed-firmware/releases/tag/${DRONE_TAG})
[-How to install firmware-](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/HowToInstall.md)
[-Download latest extra apps pack-](https://download-directory.github.io/?url=https://github.com/xMasterX/unleashed-extra-pack/tree/main/apps)
[-Version without custom animations - Install via Web Updater-](https://lab.flipper.net/?url=https://unleashedflip.com/fw_no_anim/flipper-z-f7-update-${DRONE_TAG}n.tgz&channel=release-cfw&version=${DRONE_TAG}n)
[-Install via Web Updater-](https://lab.flipper.net/?url=https://unleashedflip.com/fw/${DRONE_TAG}/flipper-z-f7-update-${DRONE_TAG}.tgz&channel=release-cfw&version=${DRONE_TAG})"
document:
- ${DRONE_TAG}/flipper-z-f7-update-${DRONE_TAG}.tgz
- name: "Send discord notification"
image: appleboy/drone-discord
settings:
webhook_id:
from_secret: ds_wh_id
webhook_token:
from_secret: ds_wh_token
message: "New Unleashed firmware released!
Version: {{build.tag}}
[[Github]](https://github.com/DarkFlippers/unleashed-firmware/releases/tag/${DRONE_TAG})
[-How to install firmware-](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/HowToInstall.md)
[-Download latest extra apps pack-](https://download-directory.github.io/?url=https://github.com/xMasterX/unleashed-extra-pack/tree/main/apps)
[-Version without custom animations - Install via Web Updater-](https://lab.flipper.net/?url=https://unleashedflip.com/fw_no_anim/flipper-z-f7-update-${DRONE_TAG}n.tgz&channel=release-cfw&version=${DRONE_TAG}n)
[-Install via Web Updater-](https://lab.flipper.net/?url=https://unleashedflip.com/fw/${DRONE_TAG}/flipper-z-f7-update-${DRONE_TAG}.tgz&channel=release-cfw&version=${DRONE_TAG})"
trigger:
event:
- tag
node:
typ: haupt
---
kind: pipeline
type: docker
name: "Dev build"
steps:
- name: "Update submodules"
image: alpine/git
commands:
- git submodule sync
- git -c protocol.version=2 submodule update --init --force --recursive
- git submodule foreach git config --local gc.auto 0
- git log -1 --format='%H'
- name: "Build dev FW"
image: hfdj/fztools
pull: never
commands:
- export DIST_SUFFIX=${DRONE_BUILD_NUMBER}
- export WORKFLOW_BRANCH_OR_TAG=dev-cfw
- ./fbt COMPACT=1 DEBUG=0 updater_package
- mkdir artifacts-default
- mv dist/f7-C/* artifacts-default/
- ls -laS artifacts-default
- ls -laS artifacts-default/f7-update-${DRONE_BUILD_NUMBER}
environment:
FBT_TOOLS_CUSTOM_LINK:
from_secret: fbt_link
- name: "Bundle self-update packages"
image: kramos/alpine-zip
commands:
- cp artifacts-default/flipper-z-f7-update-${DRONE_BUILD_NUMBER}.tgz .
- rm -rf artifacts-default/f7-update-${DRONE_BUILD_NUMBER}
- ls -laS artifacts-default
- mv artifacts-default/ dev
- ls -laS dev
- name: "Clean dev folder"
image: appleboy/drone-ssh
settings:
host:
from_secret: dep_host
username:
from_secret: dep_user
password:
from_secret: dep_passwd
port:
from_secret: dep_port
command_timeout: 30s
script:
- cd web/unleashedflip.com/public_html/fw/dev && rm -f ./*
- name: "Upload default to updates srv"
image: appleboy/drone-scp
settings:
host:
from_secret: dep_host
username:
from_secret: dep_user
password:
from_secret: dep_passwd
port:
from_secret: dep_port
target:
from_secret: dep_target_new
source:
- dev/*.tgz
- dev/*.zip
- dev/*.json
- dev/*.elf
- dev/*.dfu
- dev/*.bin
- name: "Trigger update server reindex"
image: hfdj/fztools
pull: never
environment:
UPD_KEY:
from_secret: git_update_serv_token
UPD_URL:
from_secret: git_update_server_url
commands:
- curl -X POST -F 'key='$UPD_KEY'' $UPD_URL
- name: "Send files to telegram"
image: appleboy/drone-telegram
settings:
token:
from_secret: tgtoken
to:
from_secret: tgid_dev
format: markdown
message: "Unleashed firmware dev build successful!
Build: {{build.number}}
SHA: {{commit.sha}}
Commit: {{commit.message}}
[-Install via Web Updater-](https://lab.flipper.net/?url=https://unleashedflip.com/fw/dev/flipper-z-f7-update-${DRONE_BUILD_NUMBER}.tgz&channel=dev-cfw&version=${DRONE_BUILD_NUMBER})"
document:
- dev/flipper-z-f7-update-${DRONE_BUILD_NUMBER}.tgz
trigger:
branch:
- dev
event:
- push
node:
typ: haupt
-4
View File
@@ -1,4 +0,0 @@
[defaults]
inventory = inventory
host_key_checking = true
-108
View File
@@ -1,108 +0,0 @@
---
- hosts: all
become: yes
become_user: ClaraCrazy
gather_facts: True
tasks:
#########################################
# Creating a temp directory on the host #
#########################################
- name: Create a temp directory if it does not exist
ansible.builtin.file:
path: ~/temp/flipper/flipperzero-firmware/
state: directory
mode: '0777'
#########################################
# Creating a temp directory on the host #
#########################################
- name: Create a build directory if it does not exist
ansible.builtin.file:
path: ~/temp/build/
state: directory
mode: '0777'
##########################
# Checkout Flipper Repo #
##########################
- name: Checkout the flipper git repo
ansible.builtin.git:
repo: 'https://github.com/flipperdevices/flipperzero-firmware'
dest: ~/temp/flipper/flipperzero-firmware/
######################################################################
# Clean up previous firmware contetns folder before build deployment #
######################################################################
- name: Delete content & directory
file:
state: absent
path: ~/temp/flipper/flipperzero-firmware/dist/f7-D/f7-update-local
#####################
# Run Build Command #
#####################
- name: This command will change the working directory to /temp/flipper/flipperzero-firmware/
ansible.builtin.shell:
chdir: ~/temp/flipper/flipperzero-firmware/
cmd: ./fbt updater_package
##############################################
# copy firmware contents to new build folder #
##############################################
- name: copy git repo to remote server
copy:
src: ~/temp/flipper/flipperzero-firmware/dist/f7-D/f7-update-local
dest: ~/flipper/build/
remote_src: yes
###################################
# Create a .tar based on contents #
###################################
- name: Compress directory ~/flipper/build/
archive:
path: ~/flipper/build/
dest: ~/flipper/build/newbuild.tgz
# #########################
# # Create update weblink #
# #########################
# - name: deploy Docker Compose stack on remote server
# docker_compose:
# project_src: ~/srv/authelia/
# files:
# - docker-compose.yml
# recreate: always
# ##########################################
# # upload .zip to github #
# ##########################################
# - name: Delete content & directory
# file:
# state: absent
# path: ~/temp/
# #######################################
# # post new release message on githhub #
# #######################################
# - name: copy git repo to remote server
# copy:
# src: ~/temp/authelia
# dest: ~/srv/
# remote_src: yes
# ############################################
# # Post result on CMD line after deployment #
# ############################################
# - debug:
# var: ansible_facts["shell"]
# #################################################
# # clean up mess made when creating new firmware #
# # and new firmware release folder #
# #################################################
# - name: copy git repo to remote server
# copy:
# src: ~/temp/authelia
# dest: ~/srv/
# remote_src: yes
+3 -3
View File
@@ -54,7 +54,7 @@ Note: This repo is always updated with OFW & Unleashed. No need to mention all t
- SFW Mode
- Jamming Files
- Custom subghz presets
- Subghz and IR signal replication via gpio | Credits to @ankris812, exact commit lost to time as of rn
- Subghz and IR signal replication via gpio | Credits to @ankris812
- Honda Keys (CVE-2022-27254)
- NSFW Animations tied to the level system. Read more above
- New API Routes for Locale settings
@@ -65,6 +65,7 @@ Note: This repo is always updated with OFW & Unleashed. No need to mention all t
[Updated]
- All graphics
- About 1k files to speed things up a lot
- Folder handling for empty ones (Now indicate they are empty)
- Applications now use the new Locale setting
- Compiler now handles all non-compiled faps during build
@@ -95,8 +96,7 @@ Note: This repo is always updated with OFW & Unleashed. No need to mention all t
<h2 align="center">Known Bugs:</h2>
```txt
- Name Changer app crashes 50% of the time
- Some apps dont allow to be set as Favorite
- Nothing rn. Hopefully that wont change
```
----
+8
View File
@@ -165,6 +165,14 @@ Alias("fap_dist", fap_dist)
distenv.Depends(firmware_env["FW_RESOURCES"], firmware_env["FW_EXTAPPS"].resources_dist)
# Copy all faps to device
fap_deploy = distenv.PhonyTarget(
"fap_deploy",
"${PYTHON3} ${ROOT_DIR}/scripts/storage.py send ${SOURCE} /ext/apps",
source=Dir("#/assets/resources/apps"),
)
# Target for bundling core2 package for qFlipper
copro_dist = distenv.CoproBuilder(
+1 -1
View File
@@ -86,4 +86,4 @@ Small applications providing configuration for basic firmware and its services.
Utility apps not visible in other menus.
- `storage_move_to_sd` - Data migration tool for internal storage
- `updater` - Update service & application
- `updater` - updater service & application
+2 -2
View File
@@ -75,8 +75,8 @@ static const DuckyKey ducky_keys[] = {
{"BREAK", HID_KEYBOARD_PAUSE},
{"PAUSE", HID_KEYBOARD_PAUSE},
{"CAPSLOCK", HID_KEYBOARD_CAPS_LOCK},
{"DELETE", HID_KEYBOARD_DELETE},
{"BACKSPACE", HID_KEYPAD_BACKSPACE},
{"DELETE", HID_KEYBOARD_DELETE_FORWARD},
{"BACKSPACE", HID_KEYBOARD_DELETE},
{"END", HID_KEYBOARD_END},
{"ESC", HID_KEYBOARD_ESCAPE},
{"ESCAPE", HID_KEYBOARD_ESCAPE},
@@ -14,9 +14,12 @@ static const char* uart_ch[] = {"13,14", "15,16"};
static const char* flow_pins[] = {"None", "2,3", "6,7", "16,15"};
static const char* baudrate_mode[] = {"Host"};
static const uint32_t baudrate_list[] = {
1200,
2400,
4800,
9600,
19200,
28800,
38400,
57600,
115200,
@@ -1,10 +1,18 @@
#include "../infrared_i.h"
#include "../../../settings/desktop_settings/desktop_settings_app.h"
void infrared_scene_edit_rename_done_on_enter(void* context) {
Infrared* infrared = context;
Popup* popup = infrared->popup;
DesktopSettings* settings = malloc(sizeof(DesktopSettings));
DESKTOP_SETTINGS_LOAD(settings);
popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59);
if (settings->sfw_mode) {
popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59_sfw);
}
else {
popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59);
}
popup_set_header(popup, "Saved!", 5, 7, AlignLeft, AlignTop);
popup_set_callback(popup, infrared_popup_closed_callback);
@@ -13,6 +21,7 @@ void infrared_scene_edit_rename_done_on_enter(void* context) {
popup_enable_timeout(popup);
view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewPopup);
free(settings);
}
bool infrared_scene_edit_rename_done_on_event(void* context, SceneManagerEvent event) {
@@ -1,15 +1,23 @@
#include "../infrared_i.h"
#include "../../../settings/desktop_settings/desktop_settings_app.h"
void infrared_scene_learn_done_on_enter(void* context) {
Infrared* infrared = context;
Popup* popup = infrared->popup;
DesktopSettings* settings = malloc(sizeof(DesktopSettings));
DESKTOP_SETTINGS_LOAD(settings);
popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59);
if(infrared->app_state.is_learning_new_remote) {
popup_set_header(popup, "New remote\ncreated!", 0, 0, AlignLeft, AlignTop);
} else {
popup_set_header(popup, "Saved!", 5, 7, AlignLeft, AlignTop);
if (settings->sfw_mode) {
popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59_sfw);
if (infrared->app_state.is_learning_new_remote) {
popup_set_header(popup, "New remote\ncreated!", 0, 0, AlignLeft, AlignTop);
}
else {
popup_set_header(popup, "Saved!", 5, 7, AlignLeft, AlignTop);
}
}
else {
popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59);
}
popup_set_callback(popup, infrared_popup_closed_callback);
@@ -18,6 +26,7 @@ void infrared_scene_learn_done_on_enter(void* context) {
popup_enable_timeout(popup);
view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewPopup);
free(settings);
}
bool infrared_scene_learn_done_on_event(void* context, SceneManagerEvent event) {
@@ -1,4 +1,5 @@
#include "../infrared_i.h"
#include "../../../settings/desktop_settings/desktop_settings_app.h"
static void
infrared_scene_learn_success_dialog_result_callback(DialogExResult result, void* context) {
@@ -10,6 +11,8 @@ void infrared_scene_learn_success_on_enter(void* context) {
Infrared* infrared = context;
DialogEx* dialog_ex = infrared->dialog_ex;
InfraredSignal* signal = infrared->received_signal;
DesktopSettings* settings = malloc(sizeof(DesktopSettings));
DESKTOP_SETTINGS_LOAD(settings);
infrared_play_notification_message(infrared, InfraredNotificationMessageGreenOn);
@@ -47,12 +50,18 @@ void infrared_scene_learn_success_on_enter(void* context) {
dialog_ex_set_left_button_text(dialog_ex, "Retry");
dialog_ex_set_right_button_text(dialog_ex, "Save");
dialog_ex_set_center_button_text(dialog_ex, "Send");
dialog_ex_set_icon(dialog_ex, 0, 1, &I_DolphinReadingSuccess_59x63);
if (settings->sfw_mode) {
dialog_ex_set_icon(dialog_ex, 0, 1, &I_DolphinReadingSuccess_59x63_sfw);
}
else {
dialog_ex_set_icon(dialog_ex, 0, 1, &I_DolphinReadingSuccess_59x63);
}
dialog_ex_set_result_callback(dialog_ex, infrared_scene_learn_success_dialog_result_callback);
dialog_ex_set_context(dialog_ex, context);
dialog_ex_enable_extended_events(dialog_ex);
view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewDialogEx);
free(settings);
}
bool infrared_scene_learn_success_on_event(void* context, SceneManagerEvent event) {
@@ -3,6 +3,7 @@
enum SubmenuIndex {
SubmenuIndexUniversalRemotes,
SubmenuIndexLearnNewRemote,
SubmenuIndexLearnNewRemoteRaw,
SubmenuIndexSavedRemotes,
SubmenuIndexDebug
};
@@ -37,6 +38,12 @@ void infrared_scene_start_on_enter(void* context) {
infrared);
if(infrared->app_state.is_debug_enabled) {
submenu_add_item(
submenu,
"Learn New Remote RAW",
SubmenuIndexLearnNewRemoteRaw,
infrared_scene_start_submenu_callback,
infrared);
submenu_add_item(
submenu, "Debug", SubmenuIndexDebug, infrared_scene_start_submenu_callback, infrared);
}
@@ -61,7 +68,14 @@ bool infrared_scene_start_on_event(void* context, SceneManagerEvent event) {
if(submenu_index == SubmenuIndexUniversalRemotes) {
scene_manager_next_scene(scene_manager, InfraredSceneUniversal);
consumed = true;
} else if(submenu_index == SubmenuIndexLearnNewRemote) {
} else if(
submenu_index == SubmenuIndexLearnNewRemote ||
submenu_index == SubmenuIndexLearnNewRemoteRaw) {
// enable automatic signal decoding if "Learn New Remote"
// disable automatic signal decoding if "Learn New Remote (RAW)"
infrared_worker_rx_enable_signal_decoding(
infrared->worker, submenu_index == SubmenuIndexLearnNewRemote);
infrared->app_state.is_learning_new_remote = true;
scene_manager_next_scene(scene_manager, InfraredSceneLearn);
consumed = true;
+3 -3
View File
@@ -681,10 +681,10 @@ int32_t playlist_app(void* p) {
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
DialogsFileBrowserOptions browser_options;
dialog_file_browser_set_basic_options(&browser_options, PLAYLIST_EXT, &I_sub1_10px);
browser_options.hide_ext = false;
browser_options.base_path = PLAYLIST_FOLDER;
bool res = dialog_file_browser_show(dialogs, app->file_path, app->file_path, &browser_options);
const bool res =
dialog_file_browser_show(dialogs, app->file_path, app->file_path, &browser_options);
furi_record_close(RECORD_DIALOGS);
// check if a file was selected
if(!res) {
@@ -32,4 +32,3 @@ ADD_SCENE(subghz, decode_raw, DecodeRAW)
ADD_SCENE(subghz, delete_raw, DeleteRAW)
ADD_SCENE(subghz, need_saving, NeedSaving)
ADD_SCENE(subghz, rpc, Rpc)
ADD_SCENE(subghz, region_info, RegionInfo)
@@ -1,39 +0,0 @@
#include "../subghz_i.h"
#include <furi_hal_region.h>
void subghz_scene_region_info_on_enter(void* context) {
SubGhz* subghz = context;
const FuriHalRegion* const region = furi_hal_region_get();
FuriString* buffer;
buffer = furi_string_alloc();
if(region) {
furi_string_cat_printf(buffer, "Region: %s, bands:\n", region->country_code);
for(uint16_t i = 0; i < region->bands_count; ++i) {
furi_string_cat_printf(
buffer,
" %lu-%lu kHz\n",
region->bands[i].start / 1000,
region->bands[i].end / 1000);
}
} else {
furi_string_cat_printf(buffer, "Region: N/A\n");
}
widget_add_string_multiline_element(
subghz->widget, 0, 0, AlignLeft, AlignTop, FontSecondary, furi_string_get_cstr(buffer));
furi_string_free(buffer);
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdWidget);
}
bool subghz_scene_region_info_on_event(void* context, SceneManagerEvent event) {
UNUSED(context);
UNUSED(event);
return false;
}
void subghz_scene_region_info_on_exit(void* context) {
SubGhz* subghz = context;
widget_reset(subghz->widget);
}
@@ -10,7 +10,6 @@ enum SubmenuIndex {
SubmenuIndexAddManually,
SubmenuIndexFrequencyAnalyzer,
SubmenuIndexReadRAW,
SubmenuIndexShowRegionInfo
};
void subghz_scene_start_remove_advanced_preset(SubGhz* subghz) {
@@ -74,12 +73,6 @@ void subghz_scene_start_on_enter(void* context) {
SubmenuIndexFrequencyAnalyzer,
subghz_scene_start_submenu_callback,
subghz);
submenu_add_item(
subghz->submenu,
"Region Information",
SubmenuIndexShowRegionInfo,
subghz_scene_start_submenu_callback,
subghz);
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
submenu_add_item(
subghz->submenu, "Test", SubmenuIndexTest, subghz_scene_start_submenu_callback, subghz);
@@ -133,11 +126,6 @@ bool subghz_scene_start_on_event(void* context, SceneManagerEvent event) {
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexTest);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTest);
return true;
} else if(event.event == SubmenuIndexShowRegionInfo) {
scene_manager_set_scene_state(
subghz->scene_manager, SubGhzSceneStart, SubmenuIndexShowRegionInfo);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneRegionInfo);
return true;
}
}
return false;
+28 -10
View File
@@ -1,4 +1,5 @@
#include "../u2f_app_i.h"
#include "../../../settings/desktop_settings/desktop_settings_app.h"
static void u2f_scene_error_event_callback(GuiButtonType result, InputType type, void* context) {
furi_assert(context);
@@ -11,6 +12,8 @@ static void u2f_scene_error_event_callback(GuiButtonType result, InputType type,
void u2f_scene_error_on_enter(void* context) {
U2fApp* app = context;
DesktopSettings* settings = malloc(sizeof(DesktopSettings));
DESKTOP_SETTINGS_LOAD(settings);
if(app->error == U2fAppErrorNoFiles) {
widget_add_icon_element(app->widget, 0, 0, &I_SDQuestion_35x43);
@@ -26,19 +29,34 @@ void u2f_scene_error_on_enter(void* context) {
app->widget, GuiButtonTypeLeft, "Back", u2f_scene_error_event_callback, app);
} else if(app->error == U2fAppErrorCloseRpc) {
widget_add_icon_element(app->widget, 78, 0, &I_ActiveConnection_50x64);
widget_add_string_multiline_element(
app->widget, 3, 2, AlignLeft, AlignTop, FontPrimary, "Connection\nis active!");
widget_add_string_multiline_element(
app->widget,
3,
30,
AlignLeft,
AlignTop,
FontSecondary,
"Disconnect from\nPC or phone to\nuse this function.");
if (settings->sfw_mode) {
widget_add_string_multiline_element(
app->widget, 3, 2, AlignLeft, AlignTop, FontPrimary, "Connection\nis active!");
widget_add_string_multiline_element(
app->widget,
3,
30,
AlignLeft,
AlignTop,
FontSecondary,
"Disconnect from\nPC or phone to\nuse this function.");
}
else {
widget_add_string_multiline_element(
app->widget, 3, 2, AlignLeft, AlignTop, FontPrimary, "I am not\na whore!");
widget_add_string_multiline_element(
app->widget,
3,
30,
AlignLeft,
AlignTop,
FontSecondary,
"Pull out from\nPC or phone to\nuse me like this.");
}
}
view_dispatcher_switch_to_view(app->view_dispatcher, U2fAppViewError);
free(settings);
}
bool u2f_scene_error_on_event(void* context, SceneManagerEvent event) {
+689
View File
@@ -0,0 +1,689 @@
/* Copyright (C) 2023 Salvatore Sanfilippo -- All Rights Reserved
* See the LICENSE file for information about the license. */
#include <furi.h>
#include <furi_hal.h>
#include <input/input.h>
#include <gui/gui.h>
#include <stdlib.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
#include <math.h>
#ifndef PI
#define PI 3.14159265358979f
#endif
#define TAG "Asteroids" // Used for logging
#define DEBUG_MSG 1
#define SCREEN_XRES 128
#define SCREEN_YRES 64
#define GAME_START_LIVES 3
/* The game uses the OK button both to fire and to accelerate the ship.
* This makes it a lot more playable since the finger does not have to
* move between two keys. However it is important that the extra time the
* player needs to press the button to accelerate instead of just firing
* is precisely selected to provide a smooth experience. After a few
* attempts, it looks like 70 milliseconds is the right spot. */
#define SHIP_ACCELERATION_KEYPRESS_TIME 70
/* ============================ Data structures ============================= */
typedef struct Ship {
float x, /* Ship x position. */
y, /* Ship y position. */
vx, /* x velocity. */
vy, /* y velocity. */
rot; /* Current rotation. 2*PI full rotation. */
} Ship;
typedef struct Bullet {
float x, y, vx, vy; /* Fields like in ship. */
uint32_t ttl; /* Time to live, in ticks. */
} Bullet;
typedef struct Asteroid {
float x, y, vx, vy, rot, /* Fields like ship. */
rot_speed, /* Angular velocity (rot speed and sense). */
size; /* Asteroid size. */
uint8_t shape_seed; /* Seed to give random shape. */
} Asteroid;
#define MAXBUL 10 /* Max bullets on the screen. */
#define MAXAST 32 /* Max asteroids on the screen. */
#define SHIP_HIT_ANIMATION_LEN 15
typedef struct AsteroidsApp {
/* GUI */
Gui *gui;
ViewPort *view_port; /* We just use a raw viewport and we render
everything into the low level canvas. */
FuriMessageQueue *event_queue; /* Key press events go here. */
/* Game state. */
int running; /* Once false exists the app. */
bool gameover; /* Game over status. */
uint32_t ticks; /* Game ticks. Increments at each refresh. */
uint32_t score; /* Game score. */
uint32_t lives; /* Number of lives in the current game. */
uint32_t ship_hit; /* When non zero, the ship was hit by an asteroid
and we need to show an animation as long as
its value is non-zero (and decrease it's value
at each tick of animation). */
/* Ship state. */
struct Ship ship;
/* Bullets state. */
struct Bullet bullets[MAXBUL]; /* Each bullet state. */
int bullets_num; /* Active bullets. */
uint32_t last_bullet_tick; /* Tick the last bullet was fired. */
/* Asteroids state. */
Asteroid asteroids[MAXAST]; /* Each asteroid state. */
int asteroids_num; /* Active asteroids. */
uint32_t pressed[InputKeyMAX]; /* pressed[id] is true if pressed.
Each array item contains the time
in milliseconds the key was pressed. */
bool fire; /* Short press detected: fire a bullet. */
} AsteroidsApp;
/* ============================== Prototypes ================================ */
// Only functions called before their definition are here.
void restart_game_after_gameover(AsteroidsApp *app);
uint32_t key_pressed_time(AsteroidsApp *app, InputKey key);
/* ============================ 2D drawing ================================== */
/* This structure represents a polygon of at most POLY_MAX points.
* The function draw_poly() is able to render it on the screen, rotated
* by the amount specified. */
#define POLY_MAX 8
typedef struct Poly {
float x[POLY_MAX];
float y[POLY_MAX];
uint32_t points; /* Number of points actually populated. */
} Poly;
/* Define the polygons we use. */
Poly ShipPoly = {
{-3, 0, 3},
{-3, 6, -3},
3
};
Poly ShipFirePoly = {
{-1.5, 0, 1.5},
{-3, -6, -3},
3
};
/* Rotate the point of the polygon 'poly' and store the new rotated
* polygon in 'rot'. The polygon is rotated by an angle 'a', with
* center at 0,0. */
void rotate_poly(Poly *rot, Poly *poly, float a) {
/* We want to compute sin(a) and cos(a) only one time
* for every point to rotate. It's a slow operation. */
float sin_a = (float)sin(a);
float cos_a = (float)cos(a);
for (uint32_t j = 0; j < poly->points; j++) {
rot->x[j] = poly->x[j]*cos_a - poly->y[j]*sin_a;
rot->y[j] = poly->y[j]*cos_a + poly->x[j]*sin_a;
}
rot->points = poly->points;
}
/* This is an 8 bit LFSR we use to generate a predictable and fast
* pseudorandom sequence of numbers, to give a different shape to
* each asteroid. */
void lfsr_next(unsigned char *prev) {
unsigned char lsb = *prev & 1;
*prev = *prev >> 1;
if (lsb == 1) *prev ^= 0b11000111;
*prev ^= *prev<<7; /* Mix things a bit more. */
}
/* Render the polygon 'poly' at x,y, rotated by the specified angle. */
void draw_poly(Canvas *const canvas, Poly *poly, uint8_t x, uint8_t y, float a)
{
Poly rot;
rotate_poly(&rot,poly,a);
canvas_set_color(canvas, ColorBlack);
for (uint32_t j = 0; j < rot.points; j++) {
uint32_t a = j;
uint32_t b = j+1;
if (b == rot.points) b = 0;
canvas_draw_line(canvas,x+rot.x[a],y+rot.y[a],
x+rot.x[b],y+rot.y[b]);
}
}
/* A bullet is just a + pixels pattern. A single pixel is not
* visible enough. */
void draw_bullet(Canvas *const canvas, Bullet *b) {
canvas_draw_dot(canvas,b->x-1,b->y);
canvas_draw_dot(canvas,b->x+1,b->y);
canvas_draw_dot(canvas,b->x,b->y);
canvas_draw_dot(canvas,b->x,b->y-1);
canvas_draw_dot(canvas,b->x,b->y+1);
}
/* Draw an asteroid. The asteroid shapes is computed on the fly and
* is not stored in a permanent shape structure. In order to generate
* the shape, we use an initial fixed shape that we resize according
* to the asteroid size, perturbed according to the asteroid shape
* seed, and finally draw it rotated of the right amount. */
void draw_asteroid(Canvas *const canvas, Asteroid *ast) {
Poly ap;
/* Start with what is kinda of a circle. Note that this could be
* stored into a template and copied here, to avoid computing
* sin() / cos(). But the Flipper can handle it without problems. */
uint8_t r = ast->shape_seed;
for (int j = 0; j < 8; j++) {
float a = (PI*2)/8*j;
/* Before generating the point, to make the shape unique generate
* a random factor between .7 and 1.3 to scale the distance from
* the center. However this asteroid should have its unique shape
* that remains always the same, so we use a predictable PRNG
* implemented by an 8 bit shift register. */
lfsr_next(&r);
float scaling = .7+((float)r/255*.6);
ap.x[j] = (float)sin(a) * ast->size * scaling;
ap.y[j] = (float)cos(a) * ast->size * scaling;
}
ap.points = 8;
draw_poly(canvas,&ap,ast->x,ast->y,ast->rot);
}
/* Draw small ships in the top-right part of the screen, one for
* each left live. */
void draw_left_lives(Canvas *const canvas, AsteroidsApp *app) {
int lives = app->lives;
int x = SCREEN_XRES-5;
Poly mini_ship = {
{-2, 0, 2},
{-2, 4, -2},
3
};
while(lives--) {
draw_poly(canvas,&mini_ship,x,6,PI);
x -= 6;
}
}
/* Given the current position, update it according to the velocity and
* wrap it back to the other side if the object went over the screen. */
void update_pos_by_velocity(float *x, float *y, float vx, float vy) {
/* Return back from one side to the other of the screen. */
*x += vx;
*y += vy;
if (*x >= SCREEN_XRES) *x = 0;
else if (*x < 0) *x = SCREEN_XRES-1;
if (*y >= SCREEN_YRES) *y = 0;
else if (*y < 0) *y = SCREEN_YRES-1;
}
/* Render the current game screen. */
void render_callback(Canvas *const canvas, void *ctx) {
AsteroidsApp *app = ctx;
/* Clear screen. */
canvas_set_color(canvas, ColorWhite);
canvas_draw_box(canvas, 0, 0, SCREEN_XRES-1, SCREEN_YRES-1);
/* Draw score. */
canvas_set_color(canvas, ColorBlack);
canvas_set_font(canvas, FontSecondary);
char score[32];
snprintf(score,sizeof(score),"%lu",app->score);
canvas_draw_str(canvas, 0, 8, score);
/* Draw left ships. */
draw_left_lives(canvas,app);
/* Draw ship, asteroids, bullets. */
draw_poly(canvas,&ShipPoly,app->ship.x,app->ship.y,app->ship.rot);
if (key_pressed_time(app,InputKeyOk) > SHIP_ACCELERATION_KEYPRESS_TIME)
draw_poly(canvas,&ShipFirePoly,app->ship.x,app->ship.y,app->ship.rot);
for (int j = 0; j < app->bullets_num; j++)
draw_bullet(canvas,&app->bullets[j]);
for (int j = 0; j < app->asteroids_num; j++)
draw_asteroid(canvas,&app->asteroids[j]);
/* Game over text. */
if (app->gameover) {
canvas_set_color(canvas, ColorBlack);
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 28, 35, "GAME OVER");
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas, 25, 50, "Press OK to restart");
}
}
/* ============================ Game logic ================================== */
float distance(float x1, float y1, float x2, float y2) {
float dx = x1-x2;
float dy = y1-y2;
return sqrt(dx*dx+dy*dy);
}
/* Detect a collision between the object at x1,y1 of radius r1 and
* the object at x2, y2 of radius r2. A factor < 1 will make the
* function detect the collision even if the objects are yet not
* relly touching, while a factor > 1 will make it detect the collision
* only after they are a bit overlapping. It basically is used to
* rescale the distance.
*
* Note that in this simplified 2D world, objects are all considered
* spheres (this is why this function only takes the radius). This
* is, after all, kinda accurate for asteroids, for bullets, and
* even for the ship "core" itself. */
bool objects_are_colliding(float x1, float y1, float r1,
float x2, float y2, float r2,
float factor)
{
/* The objects are colliding if the distance between object 1 and 2
* is smaller than the sum of the two radiuses r1 and r2.
* So it would be like: sqrt((x1-x2)^2+(y1-y2)^2) < r1+r2.
* However we can avoid computing the sqrt (which is slow) by
* squaring the second term and removing the square root, making
* the comparison like this:
*
* (x1-x2)^2+(y1-y2)^2 < (r1+r2)^2. */
float dx = (x1-x2)*factor;
float dy = (y1-y2)*factor;
float rsum = r1+r2;
return dx*dx+dy*dy < rsum*rsum;
}
/* Create a new bullet headed in the same direction of the ship. */
void ship_fire_bullet(AsteroidsApp *app) {
if (app->bullets_num == MAXBUL) return;
Bullet *b = &app->bullets[app->bullets_num];
b->x = app->ship.x;
b->y = app->ship.y;
b->vx = -sin(app->ship.rot);
b->vy = cos(app->ship.rot);
/* Ship should fire from its head, not in the middle. */
b->x += b->vx*5;
b->y += b->vy*5;
/* Give the bullet some velocity (for now the vector is just
* normalized to 1). */
b->vx *= 3;
b->vy *= 3;
/* It's more realistic if we add the velocity vector of the
* ship, too. Otherwise if the ship is going fast the bullets
* will be slower, which is not how the world works. */
b->vx += app->ship.vx;
b->vy += app->ship.vy;
b->ttl = 50; /* The bullet will disappear after N ticks. */
app->bullets_num++;
}
/* Remove the specified bullet by id (index in the array). */
void remove_bullet(AsteroidsApp *app, int bid) {
/* Replace the top bullet with the empty space left
* by the removal of this bullet. This way we always take the
* array dense, which is an advantage when looping. */
int n = --app->bullets_num;
if (n && bid != n) app->bullets[bid] = app->bullets[n];
}
/* Create a new asteroid, away from the ship. Return the
* pointer to the asteroid object, so that the caller can change
* certain things of the asteroid if needed. */
Asteroid *add_asteroid(AsteroidsApp *app) {
if (app->asteroids_num == MAXAST) return NULL;
float size = 4+rand()%15;
float min_distance = 20;
float x,y;
do {
x = rand() % SCREEN_XRES;
y = rand() % SCREEN_YRES;
} while(distance(app->ship.x,app->ship.y,x,y) < min_distance+size);
Asteroid *a = &app->asteroids[app->asteroids_num++];
a->x = x;
a->y = y;
a->vx = 2*(-.5 + ((float)rand()/RAND_MAX));
a->vy = 2*(-.5 + ((float)rand()/RAND_MAX));
a->size = size;
a->rot = 0;
a->rot_speed = ((float)rand()/RAND_MAX)/10;
if (app->ticks & 1) a->rot_speed = -(a->rot_speed);
a->shape_seed = rand() & 255;
return a;
}
/* Remove the specified asteroid by id (index in the array). */
void remove_asteroid(AsteroidsApp *app, int id) {
/* Replace the top asteroid with the empty space left
* by the removal of this one. This way we always take the
* array dense, which is an advantage when looping. */
int n = --app->asteroids_num;
if (n && id != n) app->asteroids[id] = app->asteroids[n];
}
/* Called when an asteroid was reached by a bullet. The asteroid
* hit is the one with the specified 'id'. */
void asteroid_was_hit(AsteroidsApp *app, int id) {
float sizelimit = 6; // Smaller than that, they disappear in one shot.
Asteroid *a = &app->asteroids[id];
/* Asteroid is large enough to break into fragments. */
float size = a->size;
float x = a->x, y = a->y;
remove_asteroid(app,id);
if (size > sizelimit) {
int max_fragments = size / sizelimit;
int fragments = 2+rand()%max_fragments;
float newsize = size/fragments;
if (newsize < 2) newsize = 2;
for (int j = 0; j < fragments; j++) {
a = add_asteroid(app);
if (a == NULL) break; // Too many asteroids on screen.
a->x = x + -(size/2) + rand() % (int)newsize;
a->y = y + -(size/2) + rand() % (int)newsize;
a->size = newsize;
}
} else {
app->score++;
}
}
/* Set game over state. When in game-over mode, the game displays a
* game over text with a background of many asteroids floating around. */
void game_over(AsteroidsApp *app) {
restart_game_after_gameover(app);
app->gameover = true;
int asteroids = 8;
while(asteroids-- && add_asteroid(app) != NULL);
}
/* Function called when a collision between the asteroid and the
* ship is detected. */
void ship_was_hit(AsteroidsApp *app) {
app->ship_hit = SHIP_HIT_ANIMATION_LEN;
if (app->lives) {
app->lives--;
} else {
game_over(app);
}
}
/* Restart game after the ship is hit. Will reset the ship position, bullets
* and asteroids to restart the game. */
void restart_game(AsteroidsApp *app) {
app->ship.x = SCREEN_XRES / 2;
app->ship.y = SCREEN_YRES / 2;
app->ship.rot = PI; /* Start headed towards top. */
app->ship.vx = 0;
app->ship.vy = 0;
app->bullets_num = 0;
app->last_bullet_tick = 0;
app->asteroids_num = 0;
}
/* Called after game over to restart the game. This function
* also calls restart_game(). */
void restart_game_after_gameover(AsteroidsApp *app) {
app->gameover = false;
app->ticks = 0;
app->score = 0;
app->ship_hit = 0;
app->lives = GAME_START_LIVES-1; /* -1 to account for current one. */
restart_game(app);
}
/* Move bullets. */
void update_bullets_position(AsteroidsApp *app) {
for (int j = 0; j < app->bullets_num; j++) {
update_pos_by_velocity(&app->bullets[j].x,&app->bullets[j].y,
app->bullets[j].vx,app->bullets[j].vy);
if (--app->bullets[j].ttl == 0) {
remove_bullet(app,j);
j--; /* Process this bullet index again: the removal will
fill it with the top bullet to take the array dense. */
}
}
}
/* Move asteroids. */
void update_asteroids_position(AsteroidsApp *app) {
for (int j = 0; j < app->asteroids_num; j++) {
update_pos_by_velocity(&app->asteroids[j].x,&app->asteroids[j].y,
app->asteroids[j].vx,app->asteroids[j].vy);
app->asteroids[j].rot += app->asteroids[j].rot_speed;
if (app->asteroids[j].rot < 0) app->asteroids[j].rot = 2*PI;
else if (app->asteroids[j].rot > 2*PI) app->asteroids[j].rot = 0;
}
}
/* Collision detection and game state update based on collisions. */
void detect_collisions(AsteroidsApp *app) {
/* Detect collision between bullet and asteroid. */
for (int j = 0; j < app->bullets_num; j++) {
Bullet *b = &app->bullets[j];
for (int i = 0; i < app->asteroids_num; i++) {
Asteroid *a = &app->asteroids[i];
if (objects_are_colliding(a->x, a->y, a->size,
b->x, b->y, 1.5, 1))
{
asteroid_was_hit(app,i);
remove_bullet(app,j);
/* The bullet no longer exist. Break the loop.
* However we want to start processing from the
* same bullet index, since now it is used by
* another bullet (see remove_bullet()). */
j--; /* Scan this j value again. */
break;
}
}
}
/* Detect collision between ship and asteroid. */
for (int j = 0; j < app->asteroids_num; j++) {
Asteroid *a = &app->asteroids[j];
if (objects_are_colliding(a->x, a->y, a->size,
app->ship.x, app->ship.y, 4, 1))
{
ship_was_hit(app);
break;
}
}
}
/* This is the main game execution function, called 10 times for
* second (with the Flipper screen latency, an higher FPS does not
* make sense). In this function we update the position of objects based
* on velocity. Detect collisions. Update the score and so forth.
*
* Each time this function is called, app->tick is incremented. */
void game_tick(void *ctx) {
AsteroidsApp *app = ctx;
/* There are two special screens:
*
* 1. Ship was hit, we frozen the game as long as ship_hit isn't zero
* again, and show an animation of a rotating ship. */
if (app->ship_hit) {
app->ship.rot += 0.5;
app->ship_hit--;
view_port_update(app->view_port);
if (app->ship_hit == 0) {
restart_game(app);
}
return;
} else if (app->gameover) {
/* 2. Game over. We need to update only background asteroids. In this
* state the game just displays a GAME OVER text with the floating
* asteroids in background. */
if (key_pressed_time(app,InputKeyOk) > 100) {
restart_game_after_gameover(app);
}
update_asteroids_position(app);
view_port_update(app->view_port);
return;
}
/* Handle key presses. */
if (app->pressed[InputKeyLeft]) app->ship.rot -= .35;
if (app->pressed[InputKeyRight]) app->ship.rot += .35;
if (key_pressed_time(app,InputKeyOk) > SHIP_ACCELERATION_KEYPRESS_TIME) {
app->ship.vx -= 0.5*(float)sin(app->ship.rot);
app->ship.vy += 0.5*(float)cos(app->ship.rot);
} else if (app->pressed[InputKeyDown]) {
app->ship.vx *= 0.75;
app->ship.vy *= 0.75;
}
/* Fire a bullet if needed. app->fire is set in
* asteroids_update_keypress_state() since depends on exact
* pressure timing. */
if (app->fire) {
uint32_t bullet_min_period = 200; // In milliseconds
uint32_t now = furi_get_tick();
if (now - app->last_bullet_tick >= bullet_min_period) {
ship_fire_bullet(app);
app->last_bullet_tick = now;
}
app->fire = false;
}
/* Update positions and detect collisions. */
update_pos_by_velocity(&app->ship.x,&app->ship.y,app->ship.vx,app->ship.vy);
update_bullets_position(app);
update_asteroids_position(app);
detect_collisions(app);
/* From time to time, create a new asteroid. The more asteroids
* already on the screen, the smaller probability of creating
* a new one. */
if (app->asteroids_num == 0 ||
(random() % 5000) < (30/(1+app->asteroids_num)))
{
add_asteroid(app);
}
app->ticks++;
view_port_update(app->view_port);
}
/* ======================== Flipper specific code =========================== */
/* Here all we do is putting the events into the queue that will be handled
* in the while() loop of the app entry point function. */
void input_callback(InputEvent* input_event, void* ctx)
{
AsteroidsApp *app = ctx;
furi_message_queue_put(app->event_queue,input_event,FuriWaitForever);
}
/* Allocate the application state and initialize a number of stuff.
* This is called in the entry point to create the application state. */
AsteroidsApp* asteroids_app_alloc() {
AsteroidsApp *app = malloc(sizeof(AsteroidsApp));
app->gui = furi_record_open(RECORD_GUI);
app->view_port = view_port_alloc();
view_port_draw_callback_set(app->view_port, render_callback, app);
view_port_input_callback_set(app->view_port, input_callback, app);
gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
app->running = 1; /* Turns 0 when back is pressed. */
restart_game_after_gameover(app);
memset(app->pressed,0,sizeof(app->pressed));
return app;
}
/* Free what the application allocated. It is not clear to me if the
* Flipper OS, once the application exits, will be able to reclaim space
* even if we forget to free something here. */
void asteroids_app_free(AsteroidsApp *app) {
furi_assert(app);
// View related.
view_port_enabled_set(app->view_port, false);
gui_remove_view_port(app->gui, app->view_port);
view_port_free(app->view_port);
furi_record_close(RECORD_GUI);
furi_message_queue_free(app->event_queue);
app->gui = NULL;
free(app);
}
/* Return the time in milliseconds the specified key is continuously
* pressed. Or 0 if it is not pressed. */
uint32_t key_pressed_time(AsteroidsApp *app, InputKey key) {
return app->pressed[key] == 0 ? 0 :
furi_get_tick() - app->pressed[key];
}
/* Handle keys interaction. */
void asteroids_update_keypress_state(AsteroidsApp *app, InputEvent input) {
if (input.type == InputTypePress) {
app->pressed[input.key] = furi_get_tick();
} else if (input.type == InputTypeRelease) {
uint32_t dur = key_pressed_time(app,input.key);
app->pressed[input.key] = 0;
if (dur < 200 && input.key == InputKeyOk) app->fire = true;
}
}
int32_t asteroids_app_entry(void* p) {
UNUSED(p);
AsteroidsApp *app = asteroids_app_alloc();
/* Create a timer. We do data analysis in the callback. */
FuriTimer *timer = furi_timer_alloc(game_tick, FuriTimerTypePeriodic, app);
furi_timer_start(timer, furi_kernel_get_tick_frequency() / 10);
/* This is the main event loop: here we get the events that are pushed
* in the queue by input_callback(), and process them one after the
* other. */
InputEvent input;
while(app->running) {
FuriStatus qstat = furi_message_queue_get(app->event_queue, &input, 100);
if (qstat == FuriStatusOk) {
if (DEBUG_MSG) FURI_LOG_E(TAG, "Main Loop - Input: type %d key %u",
input.type, input.key);
/* Handle navigation here. Then handle view-specific inputs
* in the view specific handling function. */
if (input.type == InputTypeShort &&
input.key == InputKeyBack)
{
app->running = 0;
} else {
asteroids_update_keypress_state(app,input);
}
} else {
/* Useful to understand if the app is still alive when it
* does not respond because of bugs. */
if (DEBUG_MSG) {
static int c = 0; c++;
if (!(c % 20)) FURI_LOG_E(TAG, "Loop timeout");
}
}
}
furi_timer_free(timer);
asteroids_app_free(app);
return 0;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

@@ -0,0 +1,12 @@
App(
appid="asteroids",
name="Asteroids",
apptype=FlipperAppType.EXTERNAL,
entry_point="asteroids_app_entry",
cdefines=["APP_PROTOVIEW"],
requires=["gui"],
stack_size=8*1024,
order=50,
fap_icon="appicon.png",
fap_category="Games",
)
@@ -0,0 +1 @@
.vscode
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+24
View File
@@ -0,0 +1,24 @@
# flipperzero-cli-bridge
Allows CLI control from GUI, giving untethered access to sub-ghz chat, system diagnostics, and more.
# Installation
## Easy way - get a .fap file from the releases page
Swing by the [releases](https://github.com/ranchordo/flipperzero-cli-bridge/releases) page, and download a pre-built .fap file for the latest flipperzero firmware. Use [qFlipper](https://flipperzero.one/update) to copy the .fap file into SD Card/apps/Tools/. MAKE SURE TO UPGRADE FLIPPERZERO FIRMWARE TO NEWEST VERSION BEFORE INSTALLING.
## Hard way - building from source
The following commands will (probably) not work on Windows. If you run Windows, use wsl or a linux vm or something.
```sh
git clone https://github.com/flipperdevices/flipperzero-firmware
cd ./flipperzero-firmware
git clone https://github.com/ranchordo/flipperzero-cli-bridge ./applications_user/flipperzero-cli-bridge/
./fbt fap_dist APPSRC=applications_user/flipperzero-cli-bridge
# If everything went well, the built .fap file can be found in ./dist/f7-D/apps/apps/Tools/cli_gui.fap
```
# Usage
On the flipperzero, you should be able to find a new application (CLI-GUI Bridge) under Applications->Tools. Opening it will result in a text prompt - the prompt for the command line. Enter a suitable command (quickly pressing the back button will input a space) such as `subghz chat [freq in hz, e.g. 310000000]`, etc, then navigate to and press the SAVE key. You should then see the command window. Use Up and Down to scroll, and use Left or Center to get back to the text input prompt. A quick tap of the back key while viewing the console output sends a Ctrl-C to the console.
## Exiting the app
Holding and then releasing the back key for at least a second or so (long press) will exit the app normally, meaning that the inner terminal will send Ctrl-C and close. Any sessions will be disconnected.
Holding and then releasing the OK key for at least a second or so (long press) will exit the app while keeping the terminal open. Terminal output will be cleared the next time you launch the app, but whatever command or session was running previously will be resumed. This is especially handy with subghz chat - exiting the app while keeping the terminal open will not disconnect you from the chat, and the flipper will still vibrate briefly whenever a new message comes in (even if the app is closed).
NOTE: USB functionality (qFlipper, normal USB CLI) may not work after running the app (especially after exiting without closing the terminal), simply restart your flipper and all USB functionality will return to normal.
@@ -0,0 +1,10 @@
App(
appid="cli_gui",
name="CLI-GUI Bridge",
apptype=FlipperAppType.EXTERNAL,
entry_point="cligui_main",
requires=["gui","cli"],
stack_size=8 * 1024,
fap_icon="cligui.png",
fap_category="Tools",
)
@@ -0,0 +1,109 @@
#include "cli_control.h"
#include <cli/cli.h>
#include <cli/cli_i.h>
#include <cli/cli_vcp.h>
#include "cligui_main_i.h"
#include <FreeRTOS.h>
volatile bool gotCallbackSet = false;
FuriStreamBuffer* tx_stream;
FuriStreamBuffer* rx_stream;
static FuriThread* volatile cliThread = NULL;
static void tx_handler_stdout(const char* buffer, size_t size) {
furi_stream_buffer_send(tx_stream, buffer, size, FuriWaitForever);
}
static void tx_handler(const uint8_t* buffer, size_t size) {
furi_thread_set_stdout_callback(tx_handler_stdout);
cliThread = furi_thread_get_current();
furi_stream_buffer_send(tx_stream, buffer, size, FuriWaitForever);
}
static size_t real_rx_handler(uint8_t* buffer, size_t size, uint32_t timeout) {
size_t rx_cnt = 0;
while(size > 0) {
size_t batch_size = size;
if(batch_size > 128) batch_size = 128;
size_t len = furi_stream_buffer_receive(rx_stream, buffer, batch_size, timeout);
if(len == 0) break;
size -= len;
buffer += len;
rx_cnt += len;
}
return rx_cnt;
}
static CliCommand_internal* getInternalCliCommand(Cli* cli, const char* name) {
FuriString* target_command = furi_string_alloc();
furi_string_set_str(target_command, name);
CliCommand_internal* command =
CliCommandTree_internal_get(((Cli_internal*)cli)->commands, target_command);
furi_string_free(target_command);
return command;
}
static void session_init(void) {
}
static void session_deinit(void) {
}
static bool session_connected(void) {
return true;
}
static CliSession session;
void latch_tx_handler() {
Cli* global_cli = furi_record_open(RECORD_CLI);
CliCommand_internal* help_command = getInternalCliCommand(global_cli, "help");
cliThread = help_command->context;
furi_thread_set_stdout_callback(tx_handler_stdout);
if(cliThread != NULL) {
((FuriThread_internal*)cliThread)->output.write_callback = &tx_handler_stdout;
}
rx_stream = furi_stream_buffer_alloc(128, 1);
tx_stream = furi_stream_buffer_alloc(128, 1);
session.tx = &tx_handler;
session.rx = &real_rx_handler;
session.tx_stdout = &tx_handler_stdout;
session.init = &session_init;
session.deinit = &session_deinit;
session.is_connected = &session_connected;
cli_session_close(global_cli);
cli_session_open(global_cli, &session);
// Unlock loader-lock
Loader* loader = furi_record_open(RECORD_LOADER);
Loader_internal* loader_i = (Loader_internal*)loader;
loader_i->lock_count = 0;
furi_record_close(RECORD_CLI);
furi_record_close(RECORD_LOADER);
}
void unlatch_tx_handler(bool persist) {
Cli* global_cli = furi_record_open(RECORD_CLI);
// Stash cliThread if not null
if(cliThread != NULL) {
CliCommand_internal* help_command = getInternalCliCommand(global_cli, "help");
help_command->context = cliThread;
}
// Switch to new session
if(persist) {
// Use dummy debug firmware function as is_connected
cli_vcp.is_connected = &furi_hal_version_do_i_belong_here;
} else {
// Send CTRL-C
char eot = 0x03;
furi_stream_buffer_send(rx_stream, &eot, 1, FuriWaitForever);
}
cli_session_open(global_cli, &cli_vcp);
furi_record_close(RECORD_CLI);
// Unblock waiting rx handler
furi_stream_buffer_send(rx_stream, "_", 1, FuriWaitForever);
// Reconfigure stdout_callback to cli_vcp
if(cliThread != NULL) {
((FuriThread_internal*)cliThread)->output.write_callback = cli_vcp.tx_stdout;
}
// At this point, all cli_vcp functions should be back.
furi_stream_buffer_free(rx_stream);
furi_stream_buffer_free(tx_stream);
}
@@ -0,0 +1,8 @@
#pragma once
#include <furi.h>
#include <furi_hal.h>
extern void latch_tx_handler();
extern void unlatch_tx_handler(bool persist);
extern FuriStreamBuffer* tx_stream;
extern FuriStreamBuffer* rx_stream;
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

@@ -0,0 +1,131 @@
#include "cligui_main_i.h"
#include "cli_control.h"
#include "text_input.h"
#include "console_output.h"
static bool cligui_custom_event_cb(void* context, uint32_t event) {
UNUSED(event);
CliguiApp* app = context;
UNUSED(app);
return true;
}
static bool cligui_back_event_cb(void* context) {
CliguiApp* app = context;
UNUSED(app);
return true;
}
static void cligui_tick_event_cb(void* context) {
CliguiApp* app = context;
size_t available = furi_stream_buffer_bytes_available(app->data->streams.app_rx);
for(size_t i = 0; i < available; i++) {
char c = 0;
size_t len = furi_stream_buffer_receive(app->data->streams.app_rx, &c, 1, 100);
if(len > 0) {
furi_string_push_back(app->text_box_store, c);
}
}
if (available > 0) {
text_box_set_text(app->text_box, furi_string_get_cstr(app->text_box_store));
}
// Set input header stuff
size_t len = furi_string_size(app->text_box_store);
size_t idx = len - 2;
while (idx > 0) {
if (furi_string_get_char(app->text_box_store, idx) == '\n') {
idx++;
break;
}
idx--;
}
text_input_set_header_text(app->text_input, furi_string_get_cstr(app->text_box_store) + idx);
UNUSED(app);
}
ViewPortInputCallback prev_input_callback;
volatile bool persistent_exit = false;
static void input_callback_wrapper(InputEvent* event, void* context) {
CliguiApp* app = context;
if(event->type == InputTypeLong && event->key == InputKeyBack) {
persistent_exit = false;
view_dispatcher_stop(app->view_dispatcher);
}
if(event->type == InputTypeLong && event->key == InputKeyOk) {
persistent_exit = true;
view_dispatcher_stop(app->view_dispatcher);
}
if(app->data->state == ViewTextInput) {
text_input_input_handler(app, event);
} else {
console_output_input_handler(app, event);
}
prev_input_callback(event, app->view_dispatcher);
}
int32_t cligui_main(void* p) {
UNUSED(p);
CliguiApp* cligui = malloc(sizeof(CliguiApp));
cligui->data = malloc(sizeof(CliguiData));
latch_tx_handler();
cligui->data->streams.app_tx = rx_stream;
cligui->data->streams.app_rx = tx_stream;
cligui->gui = furi_record_open(RECORD_GUI);
cligui->view_dispatcher = view_dispatcher_alloc();
cligui->view_dispatcher_i = (ViewDispatcher_internal*)(cligui->view_dispatcher);
prev_input_callback =
((ViewPort_internal*)cligui->view_dispatcher_i->view_port)->input_callback;
view_port_input_callback_set(
cligui->view_dispatcher_i->view_port, input_callback_wrapper, cligui);
view_dispatcher_enable_queue(cligui->view_dispatcher);
view_dispatcher_set_event_callback_context(cligui->view_dispatcher, cligui);
view_dispatcher_set_custom_event_callback(cligui->view_dispatcher, cligui_custom_event_cb);
view_dispatcher_set_navigation_event_callback(cligui->view_dispatcher, cligui_back_event_cb);
view_dispatcher_set_tick_event_callback(cligui->view_dispatcher, cligui_tick_event_cb, 100);
view_dispatcher_attach_to_gui(
cligui->view_dispatcher, cligui->gui, ViewDispatcherTypeFullscreen);
view_dispatcher_send_to_front(cligui->view_dispatcher);
cligui->text_box = text_box_alloc();
view_dispatcher_add_view(
cligui->view_dispatcher, ViewConsoleOutput, text_box_get_view(cligui->text_box));
cligui->text_box_store = furi_string_alloc();
furi_string_reserve(cligui->text_box_store, TEXT_BOX_STORE_SIZE);
furi_string_set_char(cligui->text_box_store, 0, 0);
text_box_set_text(cligui->text_box, furi_string_get_cstr(cligui->text_box_store));
text_box_set_focus(cligui->text_box, TextBoxFocusEnd);
cligui->text_input = text_input_alloc();
text_input_set_result_callback(
cligui->text_input,
text_input_result_callback,
cligui,
cligui->text_input_store,
TEXT_INPUT_STORE_SIZE,
true);
view_dispatcher_add_view(
cligui->view_dispatcher, ViewTextInput, text_input_get_view(cligui->text_input));
view_dispatcher_switch_to_view(cligui->view_dispatcher, ViewTextInput);
cligui->data->state = ViewTextInput;
view_dispatcher_run(cligui->view_dispatcher);
view_dispatcher_remove_view(cligui->view_dispatcher, ViewConsoleOutput);
view_dispatcher_remove_view(cligui->view_dispatcher, ViewTextInput);
text_box_free(cligui->text_box);
furi_string_free(cligui->text_box_store);
text_input_free(cligui->text_input);
view_dispatcher_free(cligui->view_dispatcher);
unlatch_tx_handler(persistent_exit);
furi_record_close(RECORD_GUI);
free(cligui->data);
free(cligui);
return 0;
}
@@ -0,0 +1,41 @@
#pragma once
#include <furi.h>
#include <furi_hal.h>
#include <furi_hal_version.h>
#include <furi_hal_usb_cdc.h>
#include <furi_hal_usb.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/modules/text_box.h>
#include <gui/modules/text_input.h>
#include <m-dict.h>
#include <loader/loader.h>
#include "internal_defs.h"
#define TEXT_BOX_STORE_SIZE (4096)
#define TEXT_INPUT_STORE_SIZE (512)
typedef enum {
ViewTextInput,
ViewConsoleOutput,
} CliguiState;
typedef struct {
CliguiState state;
struct {
FuriStreamBuffer* app_tx;
FuriStreamBuffer* app_rx;
} streams;
} CliguiData;
typedef struct {
CliguiData* data;
Gui* gui;
TextBox* text_box;
FuriString* text_box_store;
char text_input_store[TEXT_INPUT_STORE_SIZE + 1];
TextInput* text_input;
ViewDispatcher* view_dispatcher;
ViewDispatcher_internal* view_dispatcher_i;
} CliguiApp;
@@ -0,0 +1,13 @@
#include "console_output.h"
void console_output_input_handler(CliguiApp* app, InputEvent* event) {
if(event->type == InputTypeShort && (event->key == InputKeyOk || event->key == InputKeyLeft)) {
view_dispatcher_switch_to_view(app->view_dispatcher, ViewTextInput);
app->data->state = ViewTextInput;
}
if(event->type == InputTypeShort && event->key == InputKeyBack) {
char eot = 0x03;
furi_stream_buffer_send(app->data->streams.app_tx, &eot, 1, FuriWaitForever);
}
}
@@ -0,0 +1,4 @@
#pragma once
#include "cligui_main_i.h"
extern void console_output_input_handler(CliguiApp*, InputEvent*);
@@ -0,0 +1,118 @@
#pragma once
#include <furi.h>
#include <furi_hal.h>
#include <m-dict.h>
#include <m-bptree.h>
#include <m-array.h>
#include <cli/cli.h>
typedef struct {
FuriThreadStdoutWriteCallback write_callback;
FuriString* buffer;
} FuriThreadStdout_internal;
typedef struct {
bool is_service;
FuriThreadState state;
int32_t ret;
FuriThreadCallback callback;
void* context;
FuriThreadStateCallback state_callback;
void* state_context;
char* name;
configSTACK_DEPTH_TYPE stack_size;
FuriThreadPriority priority;
TaskHandle_t task_handle;
bool heap_trace_enabled;
size_t heap_size;
FuriThreadStdout_internal output;
} FuriThread_internal;
DICT_DEF2(ViewDict, uint32_t, M_DEFAULT_OPLIST, View*, M_PTR_OPLIST)
typedef struct {
FuriMessageQueue* queue;
Gui* gui;
ViewPort* view_port;
ViewDict_t views;
View* current_view;
View* ongoing_input_view;
uint8_t ongoing_input;
ViewDispatcherCustomEventCallback custom_event_callback;
ViewDispatcherNavigationEventCallback navigation_event_callback;
ViewDispatcherTickEventCallback tick_event_callback;
uint32_t tick_period;
void* event_context;
} ViewDispatcher_internal;
typedef struct {
Gui* gui;
bool is_enabled;
ViewPortOrientation orientation;
uint8_t width;
uint8_t height;
ViewPortDrawCallback draw_callback;
void* draw_callback_context;
ViewPortInputCallback input_callback;
void* input_callback_context;
} ViewPort_internal;
typedef struct {
FuriThreadId loader_thread;
const void* application;
FuriThread* application_thread;
char* application_arguments;
void* cli;
void* gui;
void* view_dispatcher;
void* primary_menu;
void* plugins_menu;
void* debug_menu;
void* settings_menu;
volatile uint8_t lock_count;
void* pubsub;
} Loader_internal;
typedef struct {
CliCallback callback;
void* context;
uint32_t flags;
} CliCommand_internal;
#define CLI_COMMANDS_TREE_RANK 4
BPTREE_DEF2(
CliCommandTree_internal,
CLI_COMMANDS_TREE_RANK,
FuriString*,
FURI_STRING_OPLIST,
CliCommand_internal,
M_POD_OPLIST)
#define M_OPL_CliCommandTree_internal_t() BPTREE_OPLIST(CliCommandTree_internal, M_POD_OPLIST)
typedef struct {
CliCommandTree_internal_t commands;
void* mutex;
void* idle_sem;
void* last_line;
void* line;
void* session;
size_t cursor_position;
} Cli_internal;
@@ -0,0 +1,31 @@
#include "text_input.h"
#include "cligui_main_i.h"
void text_input_result_callback(void* ctx) {
CliguiApp* app = ctx;
char* data = app->text_input_store;
size_t len = strlen(data);
for(size_t i = 0; i < len; i++) {
if(data[i] >= 0x41 && data[i] <= 0x5A) {
// Char is uppercase
data[i] += 0x20;
}
}
furi_stream_buffer_send(app->data->streams.app_tx, data, len, FuriWaitForever);
furi_stream_buffer_send(app->data->streams.app_tx, "\r\n", 2, FuriWaitForever);
data[0] = 0;
view_dispatcher_switch_to_view(app->view_dispatcher, ViewConsoleOutput);
app->data->state = ViewConsoleOutput;
}
void text_input_input_handler(CliguiApp* app, InputEvent* event) {
UNUSED(app);
UNUSED(event);
if(event->type == InputTypeShort && event->key == InputKeyBack) {
// view_dispatcher_switch_to_view(app->view_dispatcher, ViewConsoleOutput);
// app->data->state = ViewConsoleOutput;
size_t len = strlen(app->text_input_store);
app->text_input_store[len] = ' ';
app->text_input_store[len + 1] = 0;
}
}
@@ -0,0 +1,5 @@
#pragma once
#include "cligui_main_i.h"
extern void text_input_result_callback(void* ctx);
extern void text_input_input_handler(CliguiApp*, InputEvent*);
+26 -9
View File
@@ -16,6 +16,8 @@
#include <dialogs/dialogs.h>
#include "DAP_Link_icons.h"
#include "../../settings/desktop_settings/desktop_settings_app.h"
/***************************************************************************/
/****************************** DAP COMMON *********************************/
/***************************************************************************/
@@ -482,22 +484,37 @@ DapConfig* dap_app_get_config(DapApp* app) {
int32_t dap_link_app(void* p) {
UNUSED(p);
DesktopSettings* settings = malloc(sizeof(DesktopSettings));
DESKTOP_SETTINGS_LOAD(settings);
if(furi_hal_usb_is_locked()) {
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
DialogMessage* message = dialog_message_alloc();
dialog_message_set_header(message, "Connection\nis active!", 3, 2, AlignLeft, AlignTop);
dialog_message_set_text(
message,
"Disconnect from\nPC or phone to\nuse this function.",
3,
30,
AlignLeft,
AlignTop);
if (settings->sfw_mode) {
dialog_message_set_header(message, "Connection\nis active!", 3, 2, AlignLeft, AlignTop);
dialog_message_set_text(
message,
"Disconnect from\nPC or phone to\nuse this function.",
3,
30,
AlignLeft,
AlignTop);
}
else {
dialog_message_set_header(message, "I am not\na whore!", 3, 2, AlignLeft, AlignTop);
dialog_message_set_text(
message,
"Pull out from\nPC or phone to\nuse me like this.",
3,
30,
AlignLeft,
AlignTop);
}
dialog_message_set_icon(message, &I_ActiveConnection_50x64, 78, 0);
dialog_message_show(dialogs, message);
dialog_message_free(message);
furi_record_close(RECORD_DIALOGS);
free(settings);
return -1;
}
@@ -524,4 +541,4 @@ int32_t dap_link_app(void* p) {
dap_app_free(app);
return 0;
}
}
@@ -1,6 +1,6 @@
App(
appid="Flashlight",
name="Flashlight",
name="[GPIO] Flashlight",
apptype=FlipperAppType.EXTERNAL,
entry_point="flashlight_app",
cdefines=["APP_FLASHLIGHT"],
@@ -1,6 +1,6 @@
App(
appid="I2C_Tools",
name="i2c Tools",
name="[GPIO] i2c Tools",
apptype=FlipperAppType.EXTERNAL,
entry_point="i2ctools_app",
cdefines=["APP_I2CTOOLS"],
Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 B

@@ -1,12 +0,0 @@
App(
appid="2048",
name="2048 (Original)",
apptype=FlipperAppType.EXTERNAL,
entry_point="game_2048_app",
cdefines=["APP_2048_GAME"],
requires=["gui"],
stack_size=2 * 1024,
order=10,
fap_icon="2048.png",
fap_category="Games",
)
-155
View File
@@ -1,155 +0,0 @@
#include <stdbool.h>
#include <stdint.h>
#include <gui/canvas.h>
/* 7px 3 width digit font by Sefjor
* digit encoding example
*7 ¦¦¦ 111
*6 ¦ ¦ 101
*5 ¦ ¦ 101
*4 ¦ ¦ 101
*3 ¦ ¦ 101
*2 ¦ ¦ 101
*1 ¦¦¦ 111
*0 000 this string is empty, used to align
* ? ? ?
* FE 82 FE //0
*/
static uint8_t font[10][3] = {
{0xFE, 0x82, 0xFE}, // 0;
{0x00, 0xFE, 0x00}, // 1;
{0xF2, 0x92, 0x9E}, // 2;
{0x92, 0x92, 0xFE}, // 3;
{0x1E, 0x10, 0xFE}, // 4;
{0x9E, 0x92, 0xF2}, // 5;
{0xFE, 0x92, 0xF2}, // 6;
{0x02, 0x02, 0xFE}, // 7;
{0xFE, 0x92, 0xFE}, // 8;
{0x9E, 0x92, 0xFE}, // 9;
};
#define FONT_HEIGHT 8
#define FONT_WIDTH 3
static void game_2048_draw_black_point(Canvas* const canvas, uint8_t x, uint8_t y) {
canvas_set_color(canvas, ColorBlack);
canvas_draw_dot(canvas, x, y);
}
static void game_2048_draw_white_square(Canvas* const canvas, uint8_t x, uint8_t y) {
canvas_set_color(canvas, ColorWhite);
canvas_draw_box(canvas, x, y, 15 - 1, 15 - 3);
}
static void _game_2048_draw_column(
Canvas* const canvas,
int digit,
int coord_x,
int coord_y,
uint8_t column) {
for(int x = 0; x < FONT_HEIGHT; ++x) {
bool is_filled = (font[digit][column] >> x) & 0x1;
if(is_filled) {
game_2048_draw_black_point(canvas, coord_x, coord_y + x);
}
}
}
static uint8_t
_game_2048_draw_digit(Canvas* const canvas, uint8_t digit, uint8_t coord_x, uint8_t coord_y) {
uint8_t x_shift = 0;
if(digit != 1) {
for(int column = 0; column < FONT_WIDTH; column++) {
_game_2048_draw_column(canvas, digit, coord_x + column, coord_y, column);
}
x_shift = 3;
} else {
_game_2048_draw_column(canvas, digit, coord_x, coord_y, true);
x_shift = 1;
}
return x_shift;
}
/* We drawing text field with 1px white border
* at given coords. Total size is:
* x = 9 = 1 + 7 + 1
* y = 1 + total text width + 1
*/
/*
* Returns array of digits and it's size,
* digits should be at least 4 size
* works from 1 to 9999
*/
static void _game_2048_parse_number(uint16_t number, uint8_t* digits, uint8_t* size) {
*size = 0;
uint16_t divider = 1000;
//find first digit, result is highest divider
while(number / divider == 0) {
divider /= 10;
if(divider == 0) {
break;
}
}
for(int i = 0; divider != 0; i++) {
digits[i] = number / divider;
number %= divider;
*size += 1;
divider /= 10;
}
}
uint8_t _game_2048_calculate_shift(uint16_t num) {
uint8_t shift = 0;
switch(num) {
case 1:
shift = 7;
break;
case 2:
case 4:
case 8:
shift = 6;
break;
case 16:
shift = 5;
break;
case 32:
case 64:
shift = 4;
break;
case 128:
shift = 3;
break;
case 256:
shift = 2;
break;
case 512:
shift = 3;
break;
case 1024:
shift = 2;
break;
}
return shift;
}
void game_2048_draw_number(Canvas* const canvas, uint8_t x, uint8_t y, int number) {
uint8_t digits[4];
uint8_t size;
_game_2048_parse_number(number, digits, &size);
if(number > 512) {
game_2048_draw_white_square(canvas, x, y);
}
x += _game_2048_calculate_shift(number);
y += 4;
for(int i = 0; i < size; ++i) {
x += _game_2048_draw_digit(canvas, digits[i], x, y);
x++;
}
}
-3
View File
@@ -1,3 +0,0 @@
#include <gui/canvas.h>
void game_2048_draw_number(Canvas* const canvas, uint8_t x, uint8_t y, int number);
-495
View File
@@ -1,495 +0,0 @@
#include <stdint.h>
#include <gui/gui.h>
#include <time.h>
#include <math.h>
#include <dolphin/dolphin.h>
#include "font.h"
#define DEBUG false
/*
0 empty
1 2
2 4
3 8
4 16
5 32
6 64
7 128
8 256
9 512
10 1024
11 2048
12 4096
...
*/
typedef uint8_t cell_state;
/* DirectionLeft <--
2 2
4 4 2 2
*/
typedef enum {
DirectionIdle,
DirectionUp,
DirectionRight,
DirectionDown,
DirectionLeft,
} Direction;
typedef struct {
uint8_t y; // 0 <= y <= 3
uint8_t x; // 0 <= x <= 3
} Point;
typedef struct {
uint32_t gameScore;
uint32_t highScore;
} Score;
typedef struct {
/*
+----X
|
| field[x][y]
Y
*/
uint8_t field[4][4];
uint8_t next_field[4][4];
Score score; // original scoring
Direction direction;
/*
field {
animation-timing-function: linear;
animation-duration: 300ms;
}
*/
uint32_t animation_start_ticks;
Point keyframe_from[4][4];
Point keyframe_to[4][4];
bool debug;
} GameState;
#define XtoPx(x) (33 + x * 15)
#define YtoPx(x) (1 + y * 15)
static void game_2048_render_callback(Canvas* const canvas, ValueMutex* const vm) {
const GameState* game_state = acquire_mutex(vm, 25);
if(game_state == NULL) {
return;
}
// Before the function is called, the state is set with the canvas_reset(canvas)
if(game_state->direction == DirectionIdle) {
for(uint8_t y = 0; y < 4; y++) {
for(uint8_t x = 0; x < 4; x++) {
uint8_t field = game_state->field[y][x];
canvas_set_color(canvas, ColorBlack);
canvas_draw_frame(canvas, XtoPx(x), YtoPx(y), 16, 16);
if(field != 0) {
game_2048_draw_number(canvas, XtoPx(x), YtoPx(y), 1 << field);
}
}
}
// display score
char buffer[12];
snprintf(buffer, sizeof(buffer), "%lu", game_state->score.gameScore);
canvas_draw_str_aligned(canvas, 127, 8, AlignRight, AlignBottom, buffer);
if(game_state->score.highScore > 0) {
char buffer2[12];
snprintf(buffer2, sizeof(buffer2), "%lu", game_state->score.highScore);
canvas_draw_str_aligned(canvas, 127, 62, AlignRight, AlignBottom, buffer2);
}
} else { // if animation
// for animation
// (osKernelGetSysTimerCount() - game_state->animation_start_ticks) / osKernelGetSysTimerFreq();
// TODO: end animation event/callback/set AnimationIdle
}
release_mutex(vm, game_state);
}
static void
game_2048_input_callback(const InputEvent* const input_event, FuriMessageQueue* event_queue) {
furi_assert(event_queue);
furi_message_queue_put(event_queue, input_event, FuriWaitForever);
}
// if return false then Game Over
static bool game_2048_set_new_number(GameState* const game_state) {
uint8_t empty = 0;
for(uint8_t y = 0; y < 4; y++) {
for(uint8_t x = 0; x < 4; x++) {
if(game_state->field[y][x] == 0) {
empty++;
}
}
}
if(empty == 0) {
return false;
}
if(empty == 1) {
// If it is 1 move before losing, we help the player and get rid of randomness.
for(uint8_t y = 0; y < 4; y++) {
for(uint8_t x = 0; x < 4; x++) {
if(game_state->field[y][x] == 0) {
bool haveFour =
// +----X
// |
// | field[x][y], 0 <= x, y <= 3
// Y
// up == 4 or
(y > 0 && game_state->field[y - 1][x] == 2) ||
// right == 4 or
(x < 3 && game_state->field[y][x + 1] == 2) ||
// down == 4
(y < 3 && game_state->field[y + 1][x] == 2) ||
// left == 4
(x > 0 && game_state->field[y][x - 1] == 2);
if(haveFour) {
game_state->field[y][x] = 2;
return true;
}
game_state->field[y][x] = 1;
return true;
}
}
}
}
uint8_t target = rand() % empty;
uint8_t twoOrFore = rand() % 4 < 3;
for(uint8_t y = 0; y < 4; y++) {
for(uint8_t x = 0; x < 4; x++) {
if(game_state->field[y][x] == 0) {
if(target == 0) {
if(twoOrFore) {
game_state->field[y][x] = 1; // 2^1 == 2 75%
} else {
game_state->field[y][x] = 2; // 2^2 == 4 25%
}
goto exit;
}
target--;
}
}
}
exit:
return true;
}
// static void game_2048_process_row(uint8_t before[4], uint8_t *(after[4])) {
// // move 1 row left.
// for(uint8_t i = 0; i <= 2; i++) {
// if(before[i] != 0 && before[i] == before[i + 1]) {
// before[i]++;
// before[i + 1] = 0;
// i++;
// }
// }
// for(uint8_t i = 0, j = 0; i <= 3; i++) {
// if (before[i] != 0) {
// before[j] = before[i];
// i++;
// }
// }
// }
static void game_2048_process_move(GameState* const game_state) {
memset(game_state->next_field, 0, sizeof(game_state->next_field));
// +----X
// |
// | field[x][y], 0 <= x, y <= 3
// Y
// up
if(game_state->direction == DirectionUp) {
for(uint8_t x = 0; x < 4; x++) {
uint8_t next_y = 0;
for(int8_t y = 0; y < 4; y++) {
uint8_t field = game_state->field[y][x];
if(field == 0) {
continue;
}
if(game_state->next_field[next_y][x] == 0) {
game_state->next_field[next_y][x] = field;
continue;
}
if(field == game_state->next_field[next_y][x]) {
game_state->next_field[next_y][x]++;
game_state->score.gameScore += pow(2, game_state->next_field[next_y][x]);
if(game_state->next_field[next_y][x] == 11 && !game_state->debug) {
DOLPHIN_DEED(getRandomDeed());
} // get some xp for making a 2048 tile
next_y++;
continue;
}
next_y++;
game_state->next_field[next_y][x] = field;
}
}
}
// right
if(game_state->direction == DirectionRight) {
for(uint8_t y = 0; y < 4; y++) {
uint8_t next_x = 3;
for(int8_t x = 3; x >= 0; x--) {
uint8_t field = game_state->field[y][x];
if(field == 0) {
continue;
}
if(game_state->next_field[y][next_x] == 0) {
game_state->next_field[y][next_x] = field;
continue;
}
if(field == game_state->next_field[y][next_x]) {
game_state->next_field[y][next_x]++;
game_state->score.gameScore += pow(2, game_state->next_field[y][next_x]);
if(game_state->next_field[y][next_x] == 11 && !game_state->debug) {
DOLPHIN_DEED(getRandomDeed());
} // get some xp for making a 2048 tile
next_x--;
continue;
}
next_x--;
game_state->next_field[y][next_x] = field;
}
}
}
// down
if(game_state->direction == DirectionDown) {
for(uint8_t x = 0; x < 4; x++) {
uint8_t next_y = 3;
for(int8_t y = 3; y >= 0; y--) {
uint8_t field = game_state->field[y][x];
if(field == 0) {
continue;
}
if(game_state->next_field[next_y][x] == 0) {
game_state->next_field[next_y][x] = field;
continue;
}
if(field == game_state->next_field[next_y][x]) {
game_state->next_field[next_y][x]++;
game_state->score.gameScore += pow(2, game_state->next_field[next_y][x]);
if(game_state->next_field[next_y][x] == 11 && !game_state->debug) {
DOLPHIN_DEED(getRandomDeed());
} // get some xp for making a 2048 tile
next_y--;
continue;
}
next_y--;
game_state->next_field[next_y][x] = field;
}
}
}
// 0, 0, 1, 1
// 1, 0, 0, 0
// left
if(game_state->direction == DirectionLeft) {
for(uint8_t y = 0; y < 4; y++) {
uint8_t next_x = 0;
for(uint8_t x = 0; x < 4; x++) {
uint8_t field = game_state->field[y][x];
if(field == 0) {
continue;
}
if(game_state->next_field[y][next_x] == 0) {
game_state->next_field[y][next_x] = field;
continue;
}
if(field == game_state->next_field[y][next_x]) {
game_state->next_field[y][next_x]++;
game_state->score.gameScore += pow(2, game_state->next_field[y][next_x]);
if(game_state->next_field[y][next_x] == 11 && !game_state->debug) {
DOLPHIN_DEED(getRandomDeed());
} // get some xp for making a 2048 tile
next_x++;
continue;
}
next_x++;
game_state->next_field[y][next_x] = field;
}
}
}
// <debug>
game_state->direction = DirectionIdle;
memcpy(game_state->field, game_state->next_field, sizeof(game_state->field));
// </debug>
}
static void game_2048_restart(GameState* const game_state) {
game_state->debug = DEBUG;
// check score
if(game_state->score.gameScore > game_state->score.highScore) {
game_state->score.highScore = game_state->score.gameScore;
}
// clear all cells
for(uint8_t y = 0; y < 4; y++) {
for(uint8_t x = 0; x < 4; x++) {
game_state->field[y][x] = 0;
}
}
// start next game
game_state->score.gameScore = 0;
game_2048_set_new_number(game_state);
game_2048_set_new_number(game_state);
}
int32_t game_2048_app(void* p) {
UNUSED(p);
int32_t return_code = 0;
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
GameState* game_state = malloc(sizeof(GameState));
ValueMutex state_mutex;
if(!init_mutex(&state_mutex, game_state, sizeof(GameState))) {
return_code = 255;
goto free_and_exit;
}
ViewPort* view_port = view_port_alloc();
view_port_draw_callback_set(
view_port, (ViewPortDrawCallback)game_2048_render_callback, &state_mutex);
view_port_input_callback_set(
view_port, (ViewPortInputCallback)game_2048_input_callback, event_queue);
Gui* gui = furi_record_open(RECORD_GUI);
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
game_state->direction = DirectionIdle;
game_2048_restart(game_state);
if(game_state->debug) {
game_state->field[0][0] = 0;
game_state->field[0][1] = 0;
game_state->field[0][2] = 0;
game_state->field[0][3] = 0;
game_state->field[1][0] = 1;
game_state->field[1][1] = 2;
game_state->field[1][2] = 3;
game_state->field[1][3] = 4;
game_state->field[2][0] = 5;
game_state->field[2][1] = 6;
game_state->field[2][2] = 7;
game_state->field[2][3] = 8;
game_state->field[3][0] = 9;
game_state->field[3][1] = 10;
game_state->field[3][2] = 11;
game_state->field[3][3] = 12;
}
InputEvent event;
for(bool loop = true; loop;) {
FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
GameState* game_state = (GameState*)acquire_mutex_block(&state_mutex);
if(event_status == FuriStatusOk) {
if(event.type == InputTypeShort) {
switch(event.key) {
case InputKeyUp:
game_state->direction = DirectionUp;
game_2048_process_move(game_state);
game_2048_set_new_number(game_state);
break;
case InputKeyDown:
game_state->direction = DirectionDown;
game_2048_process_move(game_state);
game_2048_set_new_number(game_state);
break;
case InputKeyRight:
game_state->direction = DirectionRight;
game_2048_process_move(game_state);
game_2048_set_new_number(game_state);
break;
case InputKeyLeft:
game_state->direction = DirectionLeft;
game_2048_process_move(game_state);
game_2048_set_new_number(game_state);
break;
case InputKeyOk:
game_state->direction = DirectionIdle;
break;
case InputKeyBack:
loop = false;
break;
default:
break;
}
} else if(event.type == InputTypeLong) {
if(event.key == InputKeyOk) {
game_state->direction = DirectionIdle;
game_2048_restart(game_state);
}
}
}
view_port_update(view_port);
release_mutex(&state_mutex, game_state);
}
view_port_enabled_set(view_port, false);
gui_remove_view_port(gui, view_port);
furi_record_close(RECORD_GUI);
view_port_free(view_port);
delete_mutex(&state_mutex);
free_and_exit:
free(game_state);
furi_message_queue_free(event_queue);
return return_code;
}
@@ -1,6 +1,6 @@
App(
appid="GPIO_Reader_B",
name="GPIO Reader (biotinker)",
name="[GPIO] Reader (biotinker)",
apptype=FlipperAppType.EXTERNAL,
entry_point="gpio_app",
cdefines=["APP_GPIOREADER"],
@@ -1,6 +1,6 @@
App(
appid="GPIO_Reader_A",
name="GPIO Reader (Aurelilc)",
name="[GPIO] Reader (Aurelilc)",
apptype=FlipperAppType.EXTERNAL,
entry_point="GPIO_reader_app",
requires=["gui"],
@@ -312,7 +312,7 @@ int32_t music_beeper_app(void* p) {
DialogsFileBrowserOptions browser_options;
dialog_file_browser_set_basic_options(
&browser_options, MUSIC_BEEPER_APP_EXTENSION, &I_music_10px);
browser_options.hide_ext = false;
browser_options.base_path = MUSIC_BEEPER_APP_PATH_FOLDER;
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
bool res = dialog_file_browser_show(dialogs, file_path, file_path, &browser_options);
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -7,6 +7,8 @@
#include <gui/elements.h>
#include <m-array.h>
#include "../../../settings/desktop_settings/desktop_settings_app.h"
#define FRAME_HEIGHT 12
#define MAX_LEN_PX 112
#define MENU_ITEMS 4u
@@ -179,6 +181,9 @@ void pcsg_view_receiver_draw(Canvas* canvas, PCSGReceiverModel* model) {
FuriString* str_buff;
str_buff = furi_string_alloc();
DesktopSettings* settings = malloc(sizeof(DesktopSettings));
DESKTOP_SETTINGS_LOAD(settings);
PCSGReceiverMenuItem* item_menu;
for(size_t i = 0; i < MIN(model->history_item, MENU_ITEMS); ++i) {
@@ -204,7 +209,12 @@ void pcsg_view_receiver_draw(Canvas* canvas, PCSGReceiverModel* model) {
canvas_set_color(canvas, ColorBlack);
if(model->history_item == 0) {
canvas_draw_icon(canvas, 0, 0, &I_Scanning_123x52);
if (settings->sfw_mode) {
canvas_draw_icon(canvas, 0, 0, &I_Scanning_123x52_sfw);
}
else {
canvas_draw_icon(canvas, 0, 0, &I_Scanning_123x52);
}
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 63, 46, "Scanning...");
canvas_draw_line(canvas, 46, 51, 125, 51);
@@ -226,7 +236,12 @@ void pcsg_view_receiver_draw(Canvas* canvas, PCSGReceiverModel* model) {
canvas_draw_icon(canvas, 65, 42, &I_Pin_back_arrow_10x8);
canvas_draw_icon(canvas, 80, 42, &I_Pin_back_arrow_10x8);
canvas_draw_icon(canvas, 95, 42, &I_Pin_back_arrow_10x8);
canvas_draw_icon(canvas, 16, 13, &I_WarningDolphin_45x42);
if (settings->sfw_mode) {
canvas_draw_icon(canvas, 16, 13, &I_WarningDolphin_45x42_sfw);
}
else {
canvas_draw_icon(canvas, 16, 13, &I_WarningDolphin_45x42);
}
canvas_draw_dot(canvas, 17, 61);
break;
case PCSGReceiverBarShowUnlock:
@@ -239,6 +254,7 @@ void pcsg_view_receiver_draw(Canvas* canvas, PCSGReceiverModel* model) {
canvas_draw_str(canvas, 96, 62, furi_string_get_cstr(model->history_stat_str));
break;
}
free(settings);
}
static void pcsg_view_receiver_timer_callback(void* context) {
+13
View File
@@ -0,0 +1,13 @@
App(
appid="flipper_pong",
name="Pong",
apptype=FlipperAppType.EXTERNAL,
entry_point="flipper_pong_app",
cdefines=["APP_FLIPPER_PONG"],
requires=[
"gui",
],
stack_size=1 * 1024,
fap_icon="pong.png",
fap_category="Games",
)
+287
View File
@@ -0,0 +1,287 @@
// CC0 1.0 Universal (CC0 1.0)
// Public Domain Dedication
// https://github.com/nmrr
#include <stdio.h>
#include <furi.h>
#include <gui/gui.h>
#include <input/input.h>
#include <furi_hal_random.h>
#define SCREEN_SIZE_X 128
#define SCREEN_SIZE_Y 64
#define FPS 20
#define PAD_SIZE_X 3
#define PAD_SIZE_Y 8
#define PLAYER1_PAD_SPEED 2
#define PLAYER2_PAD_SPEED 2
#define BALL_SIZE 4
typedef enum {
EventTypeInput,
ClockEventTypeTick,
} EventType;
typedef struct {
EventType type;
InputEvent input;
} EventApp;
typedef struct Players
{
uint8_t player1_X,player1_Y,player2_X,player2_Y;
uint16_t player1_score,player2_score;
uint8_t ball_X,ball_Y,ball_X_speed,ball_Y_speed,ball_X_direction,ball_Y_direction;
} Players;
static void draw_callback(Canvas* canvas, void* ctx)
{
UNUSED(ctx);
Players* playersMutex = (Players*)acquire_mutex_block((ValueMutex*)ctx);
canvas_draw_frame(canvas, 0, 0, 128, 64);
canvas_draw_box(canvas, playersMutex->player1_X, playersMutex->player1_Y, PAD_SIZE_X, PAD_SIZE_Y);
canvas_draw_box(canvas, playersMutex->player2_X, playersMutex->player2_Y, PAD_SIZE_X, PAD_SIZE_Y);
canvas_draw_box(canvas, playersMutex->ball_X, playersMutex->ball_Y, BALL_SIZE, BALL_SIZE);
canvas_set_font(canvas, FontPrimary);
canvas_set_font_direction(canvas, CanvasDirectionBottomToTop);
char buffer[16];
snprintf(buffer, sizeof(buffer), "%u - %u", playersMutex->player1_score, playersMutex->player2_score);
canvas_draw_str_aligned(canvas, SCREEN_SIZE_X/2+15, SCREEN_SIZE_Y/2+2, AlignCenter, AlignTop, buffer);
release_mutex((ValueMutex*)ctx, playersMutex);
}
static void input_callback(InputEvent* input_event, void* ctx)
{
furi_assert(ctx);
FuriMessageQueue* event_queue = ctx;
EventApp event = {.type = EventTypeInput, .input = *input_event};
furi_message_queue_put(event_queue, &event, FuriWaitForever);
}
static void clock_tick(void* ctx) {
furi_assert(ctx);
FuriMessageQueue* queue = ctx;
EventApp event = {.type = ClockEventTypeTick};
furi_message_queue_put(queue, &event, 0);
}
bool insidePad(uint8_t x, uint8_t y, uint8_t playerX, uint8_t playerY)
{
if (x >= playerX && x <= playerX+PAD_SIZE_X && y >= playerY && y <= playerY+PAD_SIZE_Y) return true;
return false;
}
uint8_t changeSpeed()
{
uint8_t randomuint8[1];
while(1)
{
furi_hal_random_fill_buf(randomuint8,1);
randomuint8[0] &= 0b00000011;
if (randomuint8[0] >= 1) break;
}
return randomuint8[0];
}
uint8_t changeDirection()
{
uint8_t randomuint8[1];
furi_hal_random_fill_buf(randomuint8,1);
randomuint8[0] &= 0b1;
return randomuint8[0];
}
int32_t flipper_pong_app()
{
EventApp event;
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(EventApp));
Players players;
players.player1_X = SCREEN_SIZE_X-PAD_SIZE_X-1;
players.player1_Y = SCREEN_SIZE_Y/2 - PAD_SIZE_Y/2;
players.player1_score = 0;
players.player2_X = 1;
players.player2_Y = SCREEN_SIZE_Y/2 - PAD_SIZE_Y/2;
players.player2_score = 0;
players.ball_X = SCREEN_SIZE_X/2 - BALL_SIZE/2;
players.ball_Y = SCREEN_SIZE_Y/2 - BALL_SIZE/2;
players.ball_X_speed = 1;
players.ball_Y_speed = 1;
players.ball_X_direction = changeDirection();
players.ball_Y_direction = changeDirection();
ValueMutex state_mutex;
init_mutex(&state_mutex, &players, sizeof(Players));
ViewPort* view_port = view_port_alloc();
view_port_draw_callback_set(view_port, draw_callback, &state_mutex);
view_port_input_callback_set(view_port, input_callback, event_queue);
Gui* gui = furi_record_open(RECORD_GUI);
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
FuriTimer* timer = furi_timer_alloc(clock_tick, FuriTimerTypePeriodic, event_queue);
furi_timer_start(timer, 1000/FPS);
while(1)
{
FuriStatus event_status = furi_message_queue_get(event_queue, &event, FuriWaitForever);
Players* playersMutex = (Players*)acquire_mutex_block(&state_mutex);
if (event_status == FuriStatusOk)
{
if(event.type == EventTypeInput)
{
if(event.input.key == InputKeyBack)
{
release_mutex(&state_mutex, playersMutex);
break;
}
else if(event.input.key == InputKeyUp)
{
if (playersMutex->player1_Y >= 1+PLAYER1_PAD_SPEED) playersMutex->player1_Y -= PLAYER1_PAD_SPEED;
else playersMutex->player1_Y = 1;
}
else if(event.input.key == InputKeyDown)
{
if (playersMutex->player1_Y <= SCREEN_SIZE_Y - PAD_SIZE_Y - PLAYER1_PAD_SPEED -1) playersMutex->player1_Y += PLAYER1_PAD_SPEED;
else playersMutex->player1_Y = SCREEN_SIZE_Y - PAD_SIZE_Y - 1;
}
}
else if (event.type == ClockEventTypeTick)
{
if (playersMutex->ball_X + BALL_SIZE/2 <= SCREEN_SIZE_X*0.35 && playersMutex->ball_X_direction == 0)
{
if (playersMutex->ball_Y + BALL_SIZE/2 < playersMutex->player2_Y + PAD_SIZE_Y/2)
{
if (playersMutex->player2_Y >= 1+PLAYER2_PAD_SPEED) playersMutex->player2_Y -= PLAYER2_PAD_SPEED;
else playersMutex->player2_Y= 1;
}
else if (playersMutex->ball_Y + BALL_SIZE/2 > playersMutex->player2_Y + PAD_SIZE_Y/2)
{
if (playersMutex->player2_Y <= SCREEN_SIZE_Y - PAD_SIZE_Y - PLAYER2_PAD_SPEED -1) playersMutex->player2_Y += PLAYER2_PAD_SPEED;
else playersMutex->player2_Y = SCREEN_SIZE_Y - PAD_SIZE_Y - 1;
}
}
uint8_t ball_corner_X[4] = {playersMutex->ball_X, playersMutex->ball_X + BALL_SIZE, playersMutex->ball_X + BALL_SIZE, playersMutex->ball_X};
uint8_t ball_corner_Y[4] = {playersMutex->ball_Y, playersMutex->ball_Y, playersMutex->ball_Y + BALL_SIZE, playersMutex->ball_Y + BALL_SIZE};
bool insidePlayer1 = false, insidePlayer2 = false;
for (int i=0;i<4;i++)
{
if (insidePad(ball_corner_X[i], ball_corner_Y[i], playersMutex->player1_X, playersMutex->player1_Y) == true)
{
insidePlayer1 = true;
break;
}
if (insidePad(ball_corner_X[i], ball_corner_Y[i], playersMutex->player2_X, playersMutex->player2_Y) == true)
{
insidePlayer2 = true;
break;
}
}
if (insidePlayer1 == true)
{
playersMutex->ball_X_direction = 0;
playersMutex->ball_X -= playersMutex->ball_X_speed;
playersMutex->ball_X_speed = changeSpeed();
playersMutex->ball_Y_speed = changeSpeed();
}
else if (insidePlayer2 == true)
{
playersMutex->ball_X_direction = 1;
playersMutex->ball_X += playersMutex->ball_X_speed;
playersMutex->ball_X_speed = changeSpeed();
playersMutex->ball_Y_speed = changeSpeed();
}
else
{
if (playersMutex->ball_X_direction == 1)
{
if (playersMutex->ball_X <= SCREEN_SIZE_X - BALL_SIZE - 1 - playersMutex->ball_X_speed)
{
playersMutex->ball_X += playersMutex->ball_X_speed;
}
else
{
playersMutex->ball_X = SCREEN_SIZE_X/2 - BALL_SIZE/2;
playersMutex->ball_Y = SCREEN_SIZE_Y/2 - BALL_SIZE/2;
playersMutex->ball_X_speed = 1;
playersMutex->ball_Y_speed = 1;
playersMutex->ball_X_direction = 0;
playersMutex->player2_score++;
}
}
else
{
if (playersMutex->ball_X >= 1 + playersMutex->ball_X_speed)
{
playersMutex->ball_X -= playersMutex->ball_X_speed;
}
else
{
playersMutex->ball_X = SCREEN_SIZE_X/2 - BALL_SIZE/2;
playersMutex->ball_Y = SCREEN_SIZE_Y/2 - BALL_SIZE/2;
playersMutex->ball_X_speed = 1;
playersMutex->ball_Y_speed = 1;
playersMutex->ball_X_direction = 1;
playersMutex->player1_score++;
}
}
}
if (playersMutex->ball_Y_direction == 1)
{
if (playersMutex->ball_Y <= SCREEN_SIZE_Y - BALL_SIZE - 1 - playersMutex->ball_Y_speed)
{
playersMutex->ball_Y += playersMutex->ball_Y_speed;
}
else
{
playersMutex->ball_Y = SCREEN_SIZE_Y - BALL_SIZE - 1;
playersMutex->ball_X_speed = changeSpeed();
playersMutex->ball_Y_speed = changeSpeed();
playersMutex->ball_Y_direction = 0;
}
}
else
{
if (playersMutex->ball_Y >= 1 + playersMutex->ball_Y_speed)
{
playersMutex->ball_Y -= playersMutex->ball_Y_speed;
}
else
{
playersMutex->ball_Y = 1;
playersMutex->ball_X_speed = changeSpeed();
playersMutex->ball_Y_speed = changeSpeed();
playersMutex->ball_Y_direction = 1;
}
}
}
}
release_mutex(&state_mutex, playersMutex);
view_port_update(view_port);
}
furi_message_queue_free(event_queue);
delete_mutex(&state_mutex);
gui_remove_view_port(gui, view_port);
view_port_free(view_port);
furi_timer_free(timer);
furi_record_close(RECORD_GUI);
return 0;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

+1 -1
View File
@@ -78,7 +78,7 @@ cd ~/flipperZero/official/
git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git ./
./fbt
```
* Copy this application folder in `official/application_user`.
* Copy this application folder in `official/applications_user`.
* Connect your Flipper via USB.
* Build and install with: `./fbt launch_app APPSRC=protoview`.
+4
View File
@@ -3,9 +3,13 @@ Core improvements
- Detection of non Manchester and non RZ encoded signals. Not sure if there are any signals that are not self clocked widely used in RF. Note that the current approach already detects encodings using short high + long low and long high + short low to encode 0 and 1. In addition to the current classifier, it is possible to add one that checks for a sequence of pulses that are all multiples of some base length. This should detect, for instance, even NRZ encodings where 1 and 0 are just clocked as they are.
- Views on-enter on-exit.
Features
========
- Help screen (with press ok for next page).
- Detect the line code used and try to decode the message as hex dump.
- Pressing right/left you browse different modes:
* Current best signal pulse classes.
* Raw square wave display. Central button freezes and resumes (toggle). When frozen we display "paused" (inverted) on the low part of the screen.
+86 -323
View File
@@ -1,257 +1,42 @@
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
* See the LICENSE file for information about the license. */
#include <furi.h>
#include <furi_hal.h>
#include <lib/flipper_format/flipper_format.h>
#include <input/input.h>
#include <gui/gui.h>
#include <stdlib.h>
#include "app.h"
#include "app_buffer.h"
/* If this define is enabled, ProtoView is going to mess with the
* otherwise opaque SubGhzWorker structure in order to disable
* its filter for samples shorter than a given amount (30us at the
* time I'm writing this comment).
*
* This structure must be taken in sync with the one of the firmware. */
#define PROTOVIEW_DISABLE_SUBGHZ_FILTER 0
#ifdef PROTOVIEW_DISABLE_SUBGHZ_FILTER
struct SubGhzWorker {
FuriThread* thread;
FuriStreamBuffer* stream;
volatile bool running;
volatile bool overrun;
LevelDuration filter_level_duration;
bool filter_running;
uint16_t filter_duration;
SubGhzWorkerOverrunCallback overrun_callback;
SubGhzWorkerPairCallback pair_callback;
void* context;
};
#endif
RawSamplesBuffer *RawSamples, *DetectedSamples;
extern const SubGhzProtocolRegistry protoview_protocol_registry;
/* Render the received signal.
*
* The screen of the flipper is 128 x 64. Even using 4 pixels per line
* (where low level signal is one pixel high, high level is 4 pixels
* high) and 4 pixels of spacing between the different lines, we can
* plot comfortably 8 lines.
*
* The 'idx' argument is the first sample to render in the circular
* buffer. */
void render_signal(ProtoViewApp *app, Canvas *const canvas, RawSamplesBuffer *buf, uint32_t idx) {
canvas_set_color(canvas, ColorBlack);
int rows = 8;
uint32_t time_per_pixel = app->us_scale;
bool level = 0;
uint32_t dur = 0, sample_num = 0;
for (int row = 0; row < rows ; row++) {
for (int x = 0; x < 128; x++) {
int y = 3 + row*8;
if (dur < time_per_pixel/2) {
/* Get more data. */
raw_samples_get(buf, idx++, &level, &dur);
sample_num++;
}
canvas_draw_line(canvas, x,y,x,y-(level*3));
/* Write a small triangle under the last sample detected. */
if (app->signal_bestlen != 0 &&
sample_num == app->signal_bestlen+1)
{
canvas_draw_dot(canvas,x,y+2);
canvas_draw_dot(canvas,x-1,y+3);
canvas_draw_dot(canvas,x,y+3);
canvas_draw_dot(canvas,x+1,y+3);
sample_num++; /* Make sure we don't mark the next, too. */
}
/* Remove from the current level duration the time we
* just plot. */
if (dur > time_per_pixel)
dur -= time_per_pixel;
else
dur = 0;
}
}
}
/* Return the time difference between a and b, always >= 0 since
* the absolute value is returned. */
uint32_t duration_delta(uint32_t a, uint32_t b) {
return a > b ? a - b : b - a;
}
/* This function starts scanning samples at offset idx looking for the
* longest run of pulses, either high or low, that are among 10%
* of each other, for a maximum of three classes. The classes are
* counted separtely for high and low signals (RF on / off) because
* many devices tend to have different pulse lenghts depending on
* the level of the pulse.
*
* For instance Oregon2 sensors, in the case of protocol 2.1 will send
* pulses of ~400us (RF on) VS ~580us (RF off). */
#define SEARCH_CLASSES 3
uint32_t search_coherent_signal(RawSamplesBuffer *s, uint32_t idx) {
struct {
uint32_t dur[2]; /* dur[0] = low, dur[1] = high */
uint32_t count[2]; /* Associated observed frequency. */
} classes[SEARCH_CLASSES];
memset(classes,0,sizeof(classes));
uint32_t minlen = 40, maxlen = 4000; /* Depends on data rate, here we
allow for high and low. */
uint32_t len = 0; /* Observed len of coherent samples. */
s->short_pulse_dur = 0;
for (uint32_t j = idx; j < idx+500; j++) {
bool level;
uint32_t dur;
raw_samples_get(s, j, &level, &dur);
if (dur < minlen || dur > maxlen) break; /* return. */
/* Let's see if it matches a class we already have or if we
* can populate a new (yet empty) class. */
uint32_t k;
for (k = 0; k < SEARCH_CLASSES; k++) {
if (classes[k].count[level] == 0) {
classes[k].dur[level] = dur;
classes[k].count[level] = 1;
break; /* Sample accepted. */
} else {
uint32_t classavg = classes[k].dur[level];
uint32_t count = classes[k].count[level];
uint32_t delta = duration_delta(dur,classavg);
if (delta < classavg/10) {
/* It is useful to compute the average of the class
* we are observing. We know how many samples we got so
* far, so we can recompute the average easily.
* By always having a better estimate of the pulse len
* we can avoid missing next samples in case the first
* observed samples are too off. */
classavg = ((classavg * count) + dur) / (count+1);
classes[k].dur[level] = classavg;
classes[k].count[level]++;
break; /* Sample accepted. */
}
}
}
if (k == SEARCH_CLASSES) break; /* No match, return. */
/* If we are here, we accepted this sample. Try with the next
* one. */
len++;
}
/* Update the buffer setting the shortest pulse we found
* among the three classes. This will be used when scaling
* for visualization. */
for (int j = 0; j < SEARCH_CLASSES; j++) {
for (int level = 0; level < 2; level++) {
if (classes[j].dur[level] == 0) continue;
if (classes[j].count[level] < 3) continue;
if (s->short_pulse_dur == 0 ||
s->short_pulse_dur > classes[j].dur[level])
{
s->short_pulse_dur = classes[j].dur[level];
}
}
}
return len;
}
/* Search the buffer with the stored signal (last N samples received)
* in order to find a coherent signal. If a signal that does not appear to
* be just noise is found, it is set in DetectedSamples global signal
* buffer, that is what is rendered on the screen. */
void scan_for_signal(ProtoViewApp *app) {
/* We need to work on a copy: the RawSamples buffer is populated
* by the background thread receiving data. */
RawSamplesBuffer *copy = raw_samples_alloc();
raw_samples_copy(copy,RawSamples);
/* Try to seek on data that looks to have a regular high low high low
* pattern. */
uint32_t minlen = 13; /* Min run of coherent samples. Up to
12 samples it's very easy to mistake
noise for signal. */
uint32_t i = 0;
while (i < copy->total-1) {
uint32_t thislen = search_coherent_signal(copy,i);
if (thislen > minlen && thislen > app->signal_bestlen) {
app->signal_bestlen = thislen;
raw_samples_copy(DetectedSamples,copy);
DetectedSamples->idx = (DetectedSamples->idx+i)%
DetectedSamples->total;
FURI_LOG_E(TAG, "Displayed sample updated (%d samples)",
(int)thislen);
}
i += thislen ? thislen : 1;
}
raw_samples_free(copy);
}
/* Draw some text with a border. If the outside color is black and the inside
* color is white, it just writes the border of the text, but the function can
* also be used to write a bold variation of the font setting both the
* colors to black, or alternatively to write a black text with a white
* border so that it is visible if there are black stuff on the background. */
void canvas_draw_str_with_border(Canvas* canvas, uint8_t x, uint8_t y, const char* str, Color text_color, Color border_color)
{
struct {
uint8_t x; uint8_t y;
} dir[8] = {
{-1,-1},
{0,-1},
{1,-1},
{1,0},
{1,1},
{0,1},
{-1,1},
{-1,0}
};
/* Rotate in all the directions writing the same string to create a
* border, then write the actual string in the other color in the
* middle. */
canvas_set_color(canvas, border_color);
for (int j = 0; j < 8; j++)
canvas_draw_str(canvas,x+dir[j].x,y+dir[j].y,str);
canvas_set_color(canvas, text_color);
canvas_draw_str(canvas,x,y,str);
canvas_set_color(canvas, ColorBlack);
}
/* Raw pulses rendering. This is our default view. */
void render_view_raw_pulses(Canvas *const canvas, ProtoViewApp *app) {
/* Show signal. */
render_signal(app, canvas, DetectedSamples, 0);
/* Show signal information. */
char buf[64];
snprintf(buf,sizeof(buf),"%luus",
(unsigned long)DetectedSamples->short_pulse_dur);
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_with_border(canvas, 97, 63, buf, ColorWhite, ColorBlack);
}
/* Renders a single view with frequency and modulation setting. However
* this are logically two different views, and only one of the settings
* will be highlighted. */
void render_view_settings(Canvas *const canvas, ProtoViewApp *app) {
UNUSED(app);
canvas_set_font(canvas, FontPrimary);
if (app->current_view == ViewFrequencySettings)
canvas_draw_str_with_border(canvas,1,10,"Frequency",ColorWhite,ColorBlack);
else
canvas_draw_str(canvas,1,10,"Frequency");
if (app->current_view == ViewModulationSettings)
canvas_draw_str_with_border(canvas,70,10,"Modulation",ColorWhite,ColorBlack);
else
canvas_draw_str(canvas,70,10,"Modulation");
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas,10,61,"Use up and down to modify");
/* Show frequency. We can use big numbers font since it's just a number. */
if (app->current_view == ViewFrequencySettings) {
char buf[16];
snprintf(buf,sizeof(buf),"%.2f",(double)app->frequency/1000000);
canvas_set_font(canvas, FontBigNumbers);
canvas_draw_str(canvas, 30, 40, buf);
} else if (app->current_view == ViewModulationSettings) {
int current = app->modulation;
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 33, 39, ProtoViewModulations[current].name);
}
}
/* The callback actually just passes the control to the actual active
* view callback, after setting up basic stuff like cleaning the screen
* and setting color to black. */
@@ -267,9 +52,11 @@ static void render_callback(Canvas *const canvas, void *ctx) {
/* Call who is in charge right now. */
switch(app->current_view) {
case ViewRawPulses: render_view_raw_pulses(canvas,app); break;
case ViewInfo: render_view_info(canvas,app); break;
case ViewFrequencySettings:
case ViewModulationSettings:
render_view_settings(canvas,app); break;
case ViewDirectSampling: render_view_direct_sampling(canvas,app); break;
case ViewLast: furi_crash(TAG " ViewLast selected"); break;
}
}
@@ -279,11 +66,29 @@ static void render_callback(Canvas *const canvas, void *ctx) {
static void input_callback(InputEvent* input_event, void* ctx)
{
ProtoViewApp *app = ctx;
furi_message_queue_put(app->event_queue,input_event,FuriWaitForever);
}
if (input_event->type == InputTypePress) {
furi_message_queue_put(app->event_queue,input_event,FuriWaitForever);
FURI_LOG_E(TAG, "INPUT CALLBACK %d", (int)input_event->key);
/* Called to switch view (when left/right is pressed). Handles
* changing the current view ID and calling the enter/exit view
* callbacks if needed. */
static void app_switch_view(ProtoViewApp *app, SwitchViewDirection dir) {
ProtoViewCurrentView old = app->current_view;
if (dir == AppNextView) {
app->current_view++;
if (app->current_view == ViewLast) app->current_view = 0;
} else if (dir == AppPrevView) {
if (app->current_view == 0)
app->current_view = ViewLast-1;
else
app->current_view--;
}
ProtoViewCurrentView new = app->current_view;
/* Call the enter/exit view callbacks if needed. */
if (old == ViewDirectSampling) view_exit_direct_sampling(app);
if (new == ViewDirectSampling) view_enter_direct_sampling(app);
}
/* Allocate the application state and initialize a number of stuff.
@@ -297,7 +102,7 @@ ProtoViewApp* protoview_app_alloc() {
//init setting
app->setting = subghz_setting_alloc();
subghz_setting_load(app->setting, EXT_PATH("protoview/settings.txt"));
subghz_setting_load(app->setting, EXT_PATH("subghz/assets/setting_user"));
// GUI
app->gui = furi_record_open(RECORD_GUI);
@@ -310,13 +115,20 @@ ProtoViewApp* protoview_app_alloc() {
// Signal found and visualization defaults
app->signal_bestlen = 0;
app->us_scale = 100;
app->signal_decoded = false;
app->us_scale = PROTOVIEW_RAW_VIEW_DEFAULT_SCALE;
app->signal_offset = 0;
//init Worker & Protocol
app->txrx = malloc(sizeof(ProtoViewTxRx));
/* Setup rx worker and environment. */
app->txrx->worker = subghz_worker_alloc();
#ifdef PROTOVIEW_DISABLE_SUBGHZ_FILTER
app->txrx->worker->filter_running = 0;
#endif
app->txrx->environment = subghz_environment_alloc();
subghz_environment_set_protocol_registry(
app->txrx->environment, (void*)&protoview_protocol_registry);
@@ -380,70 +192,6 @@ static void timer_callback(void *ctx) {
scan_for_signal(app);
}
/* Handle input for the raw pulses view. */
void process_input_raw_pulses(ProtoViewApp *app, InputEvent input) {
if (input.key == InputKeyOk) {
/* Reset the current sample to capture the next. */
app->signal_bestlen = 0;
raw_samples_reset(DetectedSamples);
raw_samples_reset(RawSamples);
} else if (input.key == InputKeyDown) {
/* Rescaling. The set becomes finer under 50us per pixel. */
uint32_t scale_step = app->us_scale >= 50 ? 50 : 10;
if (app->us_scale < 500) app->us_scale += scale_step;
} else if (input.key == InputKeyUp) {
uint32_t scale_step = app->us_scale > 50 ? 50 : 10;
if (app->us_scale > 10) app->us_scale -= scale_step;
}
}
/* Handle input for the settings view. */
void process_input_settings(ProtoViewApp *app, InputEvent input) {
/* Here we handle only up and down. Avoid any work if the user
* pressed something else. */
if (input.key != InputKeyDown && input.key != InputKeyUp) return;
if (app->current_view == ViewFrequencySettings) {
size_t curidx = 0, i;
size_t count = subghz_setting_get_frequency_count(app->setting);
/* Scan the list of frequencies to check for the index of the
* currently set frequency. */
for(i = 0; i < count; i++) {
uint32_t freq = subghz_setting_get_frequency(app->setting,i);
if (freq == app->frequency) {
curidx = i;
break;
}
}
if (i == count) return; /* Should never happen. */
if (input.key == InputKeyUp) {
curidx = (curidx+1) % count;
} else if (input.key == InputKeyDown) {
curidx = curidx == 0 ? count-1 : curidx-1;
}
app->frequency = subghz_setting_get_frequency(app->setting,curidx);
} else if (app->current_view == ViewModulationSettings) {
uint32_t count = 0;
uint32_t modid = app->modulation;
while(ProtoViewModulations[count].name != NULL) count++;
if (input.key == InputKeyUp) {
modid = (modid+1) % count;
} else if (input.key == InputKeyDown) {
modid = modid == 0 ? count-1 : modid-1;
}
app->modulation = modid;
}
/* Apply changes. */
FURI_LOG_E(TAG, "Setting view, setting frequency/modulation to %lu %s", app->frequency, ProtoViewModulations[app->modulation].name);
radio_rx_end(app);
radio_begin(app);
radio_rx(app);
}
int32_t protoview_app_entry(void* p) {
UNUSED(p);
ProtoViewApp *app = protoview_app_alloc();
@@ -465,39 +213,53 @@ int32_t protoview_app_entry(void* p) {
while(app->running) {
FuriStatus qstat = furi_message_queue_get(app->event_queue, &input, 100);
if (qstat == FuriStatusOk) {
FURI_LOG_E(TAG, "Main Loop - Input: %u", input.key);
if (DEBUG_MSG) FURI_LOG_E(TAG, "Main Loop - Input: type %d key %u",
input.type, input.key);
/* Handle navigation here. Then handle view-specific inputs
* in the view specific handling function. */
if (input.key == InputKeyBack) {
if (input.type == InputTypeShort &&
input.key == InputKeyBack)
{
/* Exit the app. */
app->running = 0;
} else if (input.key == InputKeyRight) {
} else if (input.type == InputTypeShort &&
input.key == InputKeyRight)
{
/* Go to the next view. */
app->current_view++;
if (app->current_view == ViewLast) app->current_view = 0;
} else if (input.key == InputKeyLeft) {
app_switch_view(app,AppNextView);
} else if (input.type == InputTypeShort &&
input.key == InputKeyLeft)
{
/* Go to the previous view. */
if (app->current_view == 0)
app->current_view = ViewLast-1;
else
app->current_view--;
app_switch_view(app,AppPrevView);
} else {
/* This is where we pass the control to the currently
* active view input processing. */
switch(app->current_view) {
case ViewRawPulses:
process_input_raw_pulses(app,input);
break;
case ViewInfo:
process_input_info(app,input);
break;
case ViewFrequencySettings:
case ViewModulationSettings:
process_input_settings(app,input);
break;
case ViewDirectSampling:
process_input_direct_sampling(app,input);
break;
case ViewLast: furi_crash(TAG " ViewLast selected"); break;
}
}
} else {
static int c = 0;
c++;
if (!(c % 20)) FURI_LOG_E(TAG, "Loop timeout");
/* Useful to understand if the app is still alive when it
* does not respond because of bugs. */
if (DEBUG_MSG) {
static int c = 0; c++;
if (!(c % 20)) FURI_LOG_E(TAG, "Loop timeout");
}
}
view_port_update(app->view_port);
}
@@ -513,3 +275,4 @@ int32_t protoview_app_entry(void* p) {
protoview_app_free(app);
return 0;
}
+84 -1
View File
@@ -3,6 +3,11 @@
#pragma once
#include <furi.h>
#include <furi_hal.h>
#include <input/input.h>
#include <gui/gui.h>
#include <stdlib.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
@@ -15,8 +20,13 @@
#include <lib/subghz/receiver.h>
#include <lib/subghz/transmitter.h>
#include <lib/subghz/registry.h>
#include "app_buffer.h"
#define TAG "ProtoView"
#define PROTOVIEW_RAW_VIEW_DEFAULT_SCALE 100
#define BITMAP_SEEK_NOT_FOUND UINT32_MAX
#define DEBUG_MSG 1
typedef struct ProtoViewApp ProtoViewApp;
@@ -30,14 +40,23 @@ typedef enum {
/* Currently active view. */
typedef enum {
ViewRawPulses,
ViewInfo,
ViewFrequencySettings,
ViewModulationSettings,
ViewDirectSampling,
ViewLast, /* Just a sentinel to wrap around. */
} ProtoViewCurrentView;
/* Used by app_switch_view() */
typedef enum {
AppNextView,
AppPrevView
} SwitchViewDirection;
typedef struct {
const char *name;
FuriHalSubGhzPreset preset;
uint8_t *custom;
} ProtoViewModulation;
extern ProtoViewModulation ProtoViewModulations[]; /* In app_subghz.c */
@@ -54,6 +73,21 @@ struct ProtoViewTxRx {
typedef struct ProtoViewTxRx ProtoViewTxRx;
/* This stucture is filled by the decoder for specific protocols with the
* informations about the message. ProtoView will display such information
* in the message info view. */
#define PROTOVIEW_MSG_STR_LEN 32
typedef struct ProtoViewMsgInfo {
char name[PROTOVIEW_MSG_STR_LEN]; /* Protocol name and version. */
char raw[PROTOVIEW_MSG_STR_LEN]; /* Protocol specific raw representation.*/
/* The following is what the decoder wants to show to user. Each decoder
* can use the number of fileds it needs. */
char info1[PROTOVIEW_MSG_STR_LEN]; /* Protocol specific info line 1. */
char info2[PROTOVIEW_MSG_STR_LEN]; /* Protocol specific info line 2. */
char info3[PROTOVIEW_MSG_STR_LEN]; /* Protocol specific info line 3. */
uint64_t len; /* Bits consumed from the stream. */
} ProtoViewMsgInfo;
struct ProtoViewApp {
/* GUI */
Gui *gui;
@@ -66,17 +100,66 @@ struct ProtoViewApp {
ProtoViewTxRx *txrx; /* Radio state. */
SubGhzSetting *setting; /* A list of valid frequencies. */
/* Application state and config. */
/* Generic app state. */
int running; /* Once false exists the app. */
uint32_t signal_bestlen; /* Longest coherent signal observed so far. */
bool signal_decoded; /* Was the current signal decoded? */
ProtoViewMsgInfo signal_info; /* Decoded message, if signal_decoded true. */
/* Raw view apps state. */
uint32_t us_scale; /* microseconds per pixel. */
uint32_t signal_offset; /* Long press left/right panning in raw view. */
/* Configuration view app state. */
uint32_t frequency; /* Current frequency. */
uint8_t modulation; /* Current modulation ID, array index in the
ProtoViewModulations table. */
};
typedef struct ProtoViewDecoder {
const char *name; /* Protocol name. */
/* The decode function takes a buffer that is actually a bitmap, with
* high and low levels represented as 0 and 1. The number of high/low
* pulses represented by the bitmap is passed as the 'numbits' argument,
* while 'numbytes' represents the total size of the bitmap pointed by
* 'bits'. So 'numbytes' is mainly useful to pass as argument to other
* functions that perform bit extraction with bound checking, such as
* bitmap_get() and so forth. */
bool (*decode)(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info);
} ProtoViewDecoder;
extern RawSamplesBuffer *RawSamples, *DetectedSamples;
/* app_radio.c */
void radio_begin(ProtoViewApp* app);
uint32_t radio_rx(ProtoViewApp* app);
void radio_idle(ProtoViewApp* app);
void radio_rx_end(ProtoViewApp* app);
void radio_sleep(ProtoViewApp* app);
/* signal.c */
uint32_t duration_delta(uint32_t a, uint32_t b);
void reset_current_signal(ProtoViewApp *app);
void scan_for_signal(ProtoViewApp *app);
bool bitmap_get(uint8_t *b, uint32_t blen, uint32_t bitpos);
void bitmap_set(uint8_t *b, uint32_t blen, uint32_t bitpos, bool val);
void bitmap_set_pattern(uint8_t *b, uint32_t blen, const char *pat);
void bitmap_invert_bytes_bits(uint8_t *p, uint32_t len);
bool bitmap_match_bits(uint8_t *b, uint32_t blen, uint32_t bitpos, const char *bits);
uint32_t bitmap_seek_bits(uint8_t *b, uint32_t blen, uint32_t startpos, uint32_t maxbits, const char *bits);
uint32_t convert_from_line_code(uint8_t *buf, uint64_t buflen, uint8_t *bits, uint32_t len, uint32_t offset, const char *zero_pattern, const char *one_pattern);
/* view_*.c */
void render_view_raw_pulses(Canvas *const canvas, ProtoViewApp *app);
void process_input_raw_pulses(ProtoViewApp *app, InputEvent input);
void render_view_settings(Canvas *const canvas, ProtoViewApp *app);
void process_input_settings(ProtoViewApp *app, InputEvent input);
void render_view_info(Canvas *const canvas, ProtoViewApp *app);
void process_input_info(ProtoViewApp *app, InputEvent input);
void render_view_direct_sampling(Canvas *const canvas, ProtoViewApp *app);
void process_input_direct_sampling(ProtoViewApp *app, InputEvent input);
void view_enter_direct_sampling(ProtoViewApp *app);
void view_exit_direct_sampling(ProtoViewApp *app);
/* ui.c */
void canvas_draw_str_with_border(Canvas* canvas, uint8_t x, uint8_t y, const char* str, Color text_color, Color border_color);
@@ -34,6 +34,12 @@ void raw_samples_reset(RawSamplesBuffer *s) {
furi_mutex_release(s->mutex);
}
/* Set the raw sample internal index so that what is currently at
* offset 'offset', will appear to be at 0 index. */
void raw_samples_center(RawSamplesBuffer *s, uint32_t offset) {
s->idx = (s->idx+offset) % RAW_SAMPLES_NUM;
}
/* Add the specified sample in the circular buffer. */
void raw_samples_add(RawSamplesBuffer *s, bool level, uint32_t dur) {
furi_mutex_acquire(s->mutex,FuriWaitForever);
@@ -23,6 +23,7 @@ typedef struct RawSamplesBuffer {
RawSamplesBuffer *raw_samples_alloc(void);
void raw_samples_reset(RawSamplesBuffer *s);
void raw_samples_center(RawSamplesBuffer *s, uint32_t offset);
void raw_samples_add(RawSamplesBuffer *s, bool level, uint32_t dur);
void raw_samples_get(RawSamplesBuffer *s, uint32_t idx, bool *level, uint32_t *dur);
void raw_samples_copy(RawSamplesBuffer *dst, RawSamplesBuffer *src);
+18 -8
View File
@@ -2,17 +2,20 @@
* See the LICENSE file for information about the license. */
#include "app.h"
#include "custom_presets.h"
#include <flipper_format/flipper_format_i.h>
ProtoViewModulation ProtoViewModulations[] = {
{"OOK 650Khz", FuriHalSubGhzPresetOok650Async},
{"OOK 270Khz", FuriHalSubGhzPresetOok270Async},
{"2FSK 2.38Khz", FuriHalSubGhzPreset2FSKDev238Async},
{"2FSK 47.6Khz", FuriHalSubGhzPreset2FSKDev476Async},
{"MSK", FuriHalSubGhzPresetMSK99_97KbAsync},
{"GFSK", FuriHalSubGhzPresetGFSK9_99KbAsync},
{NULL, 0} /* End of list sentinel. */
{"OOK 650Khz", FuriHalSubGhzPresetOok650Async, NULL},
{"OOK 270Khz", FuriHalSubGhzPresetOok270Async, NULL},
{"2FSK 2.38Khz", FuriHalSubGhzPreset2FSKDev238Async, NULL},
{"2FSK 47.6Khz", FuriHalSubGhzPreset2FSKDev476Async, NULL},
{"MSK", FuriHalSubGhzPresetMSK99_97KbAsync, NULL},
{"GFSK", FuriHalSubGhzPresetGFSK9_99KbAsync, NULL},
{"TPMS 1 (FSK)", 0, (uint8_t*)protoview_subghz_tpms1_async_regs},
{"TPMS 2 (FSK)", 0, (uint8_t*)protoview_subghz_tpms2_async_regs},
{NULL, 0, NULL} /* End of list sentinel. */
};
/* Called after the application initialization in order to setup the
@@ -23,7 +26,14 @@ void radio_begin(ProtoViewApp* app) {
furi_assert(app);
furi_hal_subghz_reset();
furi_hal_subghz_idle();
furi_hal_subghz_load_preset(ProtoViewModulations[app->modulation].preset);
/* The CC1101 preset can be either one of the standard presets, if
* the modulation "custom" field is NULL, or a custom preset we
* defined in custom_presets.h. */
if (ProtoViewModulations[app->modulation].custom == NULL)
furi_hal_subghz_load_preset(ProtoViewModulations[app->modulation].preset);
else
furi_hal_subghz_load_custom_preset(ProtoViewModulations[app->modulation].custom);
furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow);
app->txrx->txrx_state = TxRxStateIDLE;
}
@@ -5,7 +5,7 @@ App(
entry_point="protoview_app_entry",
cdefines=["APP_PROTOVIEW"],
requires=["gui"],
stack_size=8 * 1024,
stack_size=8*1024,
order=50,
fap_icon="appicon.png",
fap_category="Tools",
@@ -0,0 +1,132 @@
#include <cc1101.h>
/* This is how to configure registers MDMCFG3 and MDMCFG4.
*
* Data rate kBaud setting:
*
* MDMCFG3 is the data rate mantissa, the exponent is in MDMCFG4,
* last 4 bits of the register.
*
* The rate (assuming 26Mhz crystal) is calculated as follows:
*
* ((256+MDMCFG3)*(2^MDMCFG4:0..3bits)) / 2^28 * 26000000.
*
* For instance for the default values of MDMCFG3 (34) and MDMCFG4 (12):
*
* ((256+34)*(2^12))/(2^28)*26000000 = 115051.2688000000, that is 115KBaud
*
* Bandwidth filter setting:
*
* BW filter as just 16 possibilities depending on how the first nibble
* (first 4 bits) of the MDMCFG4 bits are set. Instead of providing the
* formula, it is simpler to show all the values of the nibble and the
* corresponding bandwidth filter.
*
* 0 812khz
* 1 650khz
* 2 541khz
* 3 464khz
* 4 406khz
* 5 325khz
* 6 270khz
* 7 232khz
* 8 203khz
* 9 162khz
* a 135khz
* b 116khz
* c 102khz
* d 82 khz
* e 68 khz
* f 58 khz
*/
/* 20 KBaud, 2FSK, 28.56 kHz deviation, 325 Khz bandwidth filter. */
static uint8_t protoview_subghz_tpms1_async_regs[][2] = {
/* GPIO GD0 */
{CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input
/* Frequency Synthesizer Control */
{CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz
/* Packet engine */
{CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening
{CC1101_PKTCTRL1, 0x04},
// // Modem Configuration
{CC1101_MDMCFG0, 0x00},
{CC1101_MDMCFG1, 0x02},
{CC1101_MDMCFG2, 0x04}, // Format 2-FSK/FM, No preamble/sync, Disable (current optimized). Other code reading TPMS uses GFSK, but should be the same when in RX mode.
{CC1101_MDMCFG3, 0x93}, // Data rate is 20kBaud
{CC1101_MDMCFG4, 0x59}, // Rx bandwidth filter is 325 kHz
{CC1101_DEVIATN, 0x41}, // Deviation 28.56 kHz
/* Main Radio Control State Machine */
{CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us)
/* Frequency Offset Compensation Configuration */
{CC1101_FOCCFG,
0x16}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off
/* Automatic Gain Control */
{CC1101_AGCCTRL0,
0x91}, //10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary
{CC1101_AGCCTRL1,
0x00}, // 0; 0 - LNA 2 gain is decreased to minimum before decreasing LNA gain; 00 - Relative carrier sense threshold disabled; 0000 - RSSI to MAIN_TARGET
{CC1101_AGCCTRL2, 0x07}, // 00 - DVGA all; 000 - MAX LNA+LNA2; 111 - MAIN_TARGET 42 dB
/* Wake on radio and timeouts control */
{CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours
/* Frontend configuration */
{CC1101_FREND0, 0x10}, // Adjusts current TX LO buffer
{CC1101_FREND1, 0x56},
/* End */
{0, 0},
};
/* 40 KBaud, 2FSK, 19 kHz deviation, 102 Khz bandwidth filter. */
static uint8_t protoview_subghz_tpms2_async_regs[][2] = {
/* GPIO GD0 */
{CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input
/* Frequency Synthesizer Control */
{CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz
/* Packet engine */
{CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening
{CC1101_PKTCTRL1, 0x04},
// // Modem Configuration
{CC1101_MDMCFG0, 0x00},
{CC1101_MDMCFG1, 0x02},
{CC1101_MDMCFG2, 0x04}, // Format 2-FSK/FM, No preamble/sync, Disable (current optimized). Other code reading TPMS uses GFSK, but should be the same when in RX mode.
{CC1101_MDMCFG3, 0x93}, // Data rate is 40kBaud
{CC1101_MDMCFG4, 0x6A}, // 6 = BW filter 270kHz, A = Data rate exp
{CC1101_DEVIATN, 0x41}, // Deviation 19.042 kHz
/* Main Radio Control State Machine */
{CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us)
/* Frequency Offset Compensation Configuration */
{CC1101_FOCCFG,
0x16}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off
/* Automatic Gain Control */
{CC1101_AGCCTRL0,
0x91}, //10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary
{CC1101_AGCCTRL1,
0x00}, // 0; 0 - LNA 2 gain is decreased to minimum before decreasing LNA gain; 00 - Relative carrier sense threshold disabled; 0000 - RSSI to MAIN_TARGET
{CC1101_AGCCTRL2, 0x07}, // 00 - DVGA all; 000 - MAX LNA+LNA2; 111 - MAIN_TARGET 42 dB
/* Wake on radio and timeouts control */
{CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours
/* Frontend configuration */
{CC1101_FREND0, 0x10}, // Adjusts current TX LO buffer
{CC1101_FREND1, 0x56},
/* End */
{0, 0},
};
@@ -0,0 +1,44 @@
/* PT/SC remotes. Usually 443.92 Mhz OOK.
*
* This line code is used in many remotes such as Princeton chips
* named PT<number>, Silian Microelectronics SC5262 and others.
* Basically every 4 pulsee represent a bit, where 1000 means 0, and
* 1110 means 1. Usually we can read 24 bits of data.
* In this specific implementation we check for a prelude that is
* 1 bit high, 31 bits low, but the check is relaxed. */
#include "../app.h"
static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) {
if (numbits < 30) return false;
const char *sync_patterns[3] = {
"10000000000000000000000000000001", /* 30 zero bits. */
"100000000000000000000000000000001", /* 31 zero bits. */
"1000000000000000000000000000000001", /* 32 zero bits. */
};
uint32_t off;
int j;
for (j = 0; j < 3; j++) {
off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_patterns[j]);
if (off != BITMAP_SEEK_NOT_FOUND) break;
}
if (off == BITMAP_SEEK_NOT_FOUND) return false;
if (DEBUG_MSG) FURI_LOG_E(TAG, "B4B1 preamble at: %lu",off);
off += strlen(sync_patterns[j])-1;
uint8_t d[3]; /* 24 bits of data. */
uint32_t decoded =
convert_from_line_code(d,sizeof(d),bits,numbytes,off,"1000","1110");
if (DEBUG_MSG) FURI_LOG_E(TAG, "B4B1 decoded: %lu",decoded);
if (decoded != 24) return false;
snprintf(info->name,PROTOVIEW_MSG_STR_LEN,"PT/SC remote");
snprintf(info->raw,PROTOVIEW_MSG_STR_LEN,"%02X%02X%02X",d[0],d[1],d[2]);
info->len = off+(4*24);
return true;
}
ProtoViewDecoder B4B1Decoder = {
"B4B1", decode
};
@@ -0,0 +1,65 @@
/* Oregon remote termometers. Usually 443.92 Mhz OOK.
*
* The protocol is described here:
* https://wmrx00.sourceforge.net/Arduino/OregonScientific-RF-Protocols.pdf
* This implementation is not very complete. */
#include "../app.h"
static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) {
if (numbits < 32) return false;
const char *sync_pattern = "01100110" "01100110" "10010110" "10010110";
uint64_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern);
if (off == BITMAP_SEEK_NOT_FOUND) return false;
FURI_LOG_E(TAG, "Oregon2 preamble+sync found");
off += 32; /* Skip preamble. */
uint8_t buffer[8], raw[8] = {0};
uint32_t decoded =
convert_from_line_code(buffer,sizeof(buffer),bits,numbytes,off,"1001","0110");
FURI_LOG_E(TAG, "Oregon2 decoded bits: %lu", decoded);
if (decoded < 11*4) return false; /* Minimum len to extract some data. */
char temp[3] = {0}, deviceid[2] = {0}, hum[2] = {0};
for (int j = 0; j < 64; j += 4) {
uint8_t nib[1];
nib[0] = (bitmap_get(buffer,8,j+0) |
bitmap_get(buffer,8,j+1) << 1 |
bitmap_get(buffer,8,j+2) << 2 |
bitmap_get(buffer,8,j+3) << 3);
if (DEBUG_MSG) FURI_LOG_E(TAG, "Not inverted nibble[%d]: %x", j/4, (unsigned int)nib[0]);
raw[j/8] |= nib[0] << (4-(j%4));
switch(j/4) {
case 1: deviceid[0] |= nib[0]; break;
case 0: deviceid[0] |= nib[0] << 4; break;
case 3: deviceid[1] |= nib[0]; break;
case 2: deviceid[1] |= nib[0] << 4; break;
case 10: temp[0] = nib[0]; break;
/* Fixme: take the temperature sign from nibble 11. */
case 9: temp[1] = nib[0]; break;
case 8: temp[2] = nib[0]; break;
case 13: hum[0] = nib[0]; break;
case 12: hum[1] = nib[0]; break;
}
}
snprintf(info->name,sizeof(info->name),"%s","Oregon v2.1");
/* The following line crashes the Flipper because of broken
* snprintf() implementation. */
snprintf(info->raw,sizeof(info->raw),"%02X%02X%02X%02X%02X%02X%02X%02X",
raw[0],raw[1],raw[2],raw[3],raw[4],raw[5],
raw[6],raw[7]);
snprintf(info->info1,sizeof(info->info1),"Sensor ID %02X%02X",
deviceid[0], deviceid[1]);
snprintf(info->info2,sizeof(info->info2),"Temperature %d%d.%d",
temp[0],temp[1],temp[2]);
snprintf(info->info3,sizeof(info->info3),"Humidity %d%d",
hum[0],hum[1]);
return true;
}
ProtoViewDecoder Oregon2Decoder = {
"Oregon2", decode
};
@@ -0,0 +1,6 @@
11001100110011001100110011001100110011001100110011001100110 (Preamble)
10 01 01 10 10 01 01 10 (Sync)
01 10 10 01 10 01 10 01 01 10 10 01 01 10 01 10 10 01 01 10 10 01 10 01 10 01 10 01 10 01 10 01 01 10 10 01 10 01 10 01 01 10 01 10 01 10 01 10 01 10 01 10 10 01 01 10 01 10 10 01 10 01 10 01 10 01 10 01 01 10 10 01 10 01 01 10 01 10 10 01 01 10 10 01 10 01 10 01 10 01 10 01 10 01 11 0
We need to seek the following bytes: 01100110 01100110 10010110 10010110
0x66 0x66 96 96
@@ -0,0 +1,63 @@
/* Renault tires TPMS. Usually 443.92 Mhz FSK.
*
* Preamble + marshal-encoded bits. 9 Bytes in total if we don't
* count the preamble. */
#include "../app.h"
#define USE_TEST_VECTOR 0
static const char *test_vector =
"10101010" "10101010" "10101010" "10101001" // Preamble + sync.
/* The following is marshal encoded, so each two characters are
* actaully one bit. 01 = 1, 10 = 0. */
"010110010110" // Flags.
"10011001101010011001" // Pressure, multiply by 0.75 to obtain kpa.
// 244 kpa here.
"1010010110011010" // Temperature, subtract 30 to obtain celsius. 22C here.
"1001010101101001"
"0101100110010101"
"1001010101100110" // Tire ID. 0x7AD779 here.
"0101010101010101"
"0101010101010101" // Two FF bytes (usually). Unknown.
"0110010101010101"; // CRC8 with (poly 7, initialization 0).
static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) {
if (USE_TEST_VECTOR) { /* Test vector to check that decoding works. */
bitmap_set_pattern(bits,numbytes,test_vector);
numbits = strlen(test_vector);
}
if (numbits < 13*8) return false;
const char *sync_pattern = "10101010" "10101010" "10101010" "10101001";
uint64_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern);
if (off == BITMAP_SEEK_NOT_FOUND) return false;
FURI_LOG_E(TAG, "Renault TPMS preamble+sync found");
off += 32; /* Skip preamble. */
uint8_t raw[9];
uint32_t decoded =
convert_from_line_code(raw,sizeof(raw),bits,numbytes,off,
"10","01"); /* Manchester. */
FURI_LOG_E(TAG, "Renault TPMS decoded bits: %lu", decoded);
if (decoded < 8*9) return false; /* Require the full 9 bytes. */
float kpa = 0.75 *((uint32_t)((raw[0]&3)<<8) | raw[1]);
int temp = raw[2]-30;
snprintf(info->name,sizeof(info->name),"%s","Renault TPMS");
snprintf(info->raw,sizeof(info->raw),"%02X%02X%02X%02X%02X%02X%02X%02X%02X",
raw[0],raw[1],raw[2],raw[3],raw[4],raw[5],
raw[6],raw[7],raw[8]);
snprintf(info->info1,sizeof(info->info1),"Pressure %.2f kpa", (double)kpa);
snprintf(info->info2,sizeof(info->info2),"Temperature %d C", temp);
return true;
}
ProtoViewDecoder RenaultTPMSDecoder = {
"Renault TPMS", decode
};
+420
View File
@@ -0,0 +1,420 @@
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
* See the LICENSE file for information about the license. */
#include "app.h"
bool decode_signal(RawSamplesBuffer *s, uint64_t len, ProtoViewMsgInfo *info);
void initialize_msg_info(ProtoViewMsgInfo *i);
/* =============================================================================
* Raw signal detection
* ===========================================================================*/
/* Return the time difference between a and b, always >= 0 since
* the absolute value is returned. */
uint32_t duration_delta(uint32_t a, uint32_t b) {
return a > b ? a - b : b - a;
}
/* Reset the current signal, so that a new one can be detected. */
void reset_current_signal(ProtoViewApp *app) {
app->signal_bestlen = 0;
app->signal_offset = 0;
app->signal_decoded = false;
raw_samples_reset(DetectedSamples);
raw_samples_reset(RawSamples);
}
/* This function starts scanning samples at offset idx looking for the
* longest run of pulses, either high or low, that are not much different
* from each other, for a maximum of three duration classes.
* So for instance 50 successive pulses that are roughly long 340us or 670us
* will be sensed as a coherent signal (example: 312, 361, 700, 334, 667, ...)
*
* The classes are counted separtely for high and low signals (RF on / off)
* because many devices tend to have different pulse lenghts depending on
* the level of the pulse.
*
* For instance Oregon2 sensors, in the case of protocol 2.1 will send
* pulses of ~400us (RF on) VS ~580us (RF off). */
#define SEARCH_CLASSES 3
uint32_t search_coherent_signal(RawSamplesBuffer *s, uint32_t idx) {
struct {
uint32_t dur[2]; /* dur[0] = low, dur[1] = high */
uint32_t count[2]; /* Associated observed frequency. */
} classes[SEARCH_CLASSES];
memset(classes,0,sizeof(classes));
uint32_t minlen = 30, maxlen = 4000; /* Depends on data rate, here we
allow for high and low. */
uint32_t len = 0; /* Observed len of coherent samples. */
s->short_pulse_dur = 0;
for (uint32_t j = idx; j < idx+500; j++) {
bool level;
uint32_t dur;
raw_samples_get(s, j, &level, &dur);
if (dur < minlen || dur > maxlen) break; /* return. */
/* Let's see if it matches a class we already have or if we
* can populate a new (yet empty) class. */
uint32_t k;
for (k = 0; k < SEARCH_CLASSES; k++) {
if (classes[k].count[level] == 0) {
classes[k].dur[level] = dur;
classes[k].count[level] = 1;
break; /* Sample accepted. */
} else {
uint32_t classavg = classes[k].dur[level];
uint32_t count = classes[k].count[level];
uint32_t delta = duration_delta(dur,classavg);
/* Is the difference in duration between this signal and
* the class we are inspecting less than a given percentage?
* If so, accept this signal. */
if (delta < classavg/8) { /* 100%/8 = 12%. */
/* It is useful to compute the average of the class
* we are observing. We know how many samples we got so
* far, so we can recompute the average easily.
* By always having a better estimate of the pulse len
* we can avoid missing next samples in case the first
* observed samples are too off. */
classavg = ((classavg * count) + dur) / (count+1);
classes[k].dur[level] = classavg;
classes[k].count[level]++;
break; /* Sample accepted. */
}
}
}
if (k == SEARCH_CLASSES) break; /* No match, return. */
/* If we are here, we accepted this sample. Try with the next
* one. */
len++;
}
/* Update the buffer setting the shortest pulse we found
* among the three classes. This will be used when scaling
* for visualization. */
uint32_t short_dur[2] = {0,0};
for (int j = 0; j < SEARCH_CLASSES; j++) {
for (int level = 0; level < 2; level++) {
if (classes[j].dur[level] == 0) continue;
if (classes[j].count[level] < 3) continue;
if (short_dur[level] == 0 ||
short_dur[level] > classes[j].dur[level])
{
short_dur[level] = classes[j].dur[level];
}
}
}
/* Use the average between high and low short pulses duration.
* Often they are a bit different, and using the average is more robust
* when we do decoding sampling at short_pulse_dur intervals. */
if (short_dur[0] == 0) short_dur[0] = short_dur[1];
if (short_dur[1] == 0) short_dur[1] = short_dur[0];
s->short_pulse_dur = (short_dur[0]+short_dur[1])/2;
return len;
}
/* Search the buffer with the stored signal (last N samples received)
* in order to find a coherent signal. If a signal that does not appear to
* be just noise is found, it is set in DetectedSamples global signal
* buffer, that is what is rendered on the screen. */
void scan_for_signal(ProtoViewApp *app) {
/* We need to work on a copy: the RawSamples buffer is populated
* by the background thread receiving data. */
RawSamplesBuffer *copy = raw_samples_alloc();
raw_samples_copy(copy,RawSamples);
/* Try to seek on data that looks to have a regular high low high low
* pattern. */
uint32_t minlen = 13; /* Min run of coherent samples. Up to
12 samples it's very easy to mistake
noise for signal. */
ProtoViewMsgInfo *info = malloc(sizeof(ProtoViewMsgInfo));
uint32_t i = 0;
while (i < copy->total-1) {
uint32_t thislen = search_coherent_signal(copy,i);
/* For messages that are long enough, attempt decoding. */
if (thislen > minlen) {
initialize_msg_info(info);
uint32_t saved_idx = copy->idx; /* Save index, see later. */
/* decode_signal() expects the detected signal to start
* from index .*/
raw_samples_center(copy,i);
bool decoded = decode_signal(copy,thislen,info);
copy->idx = saved_idx; /* Restore the index as we are scanning
the signal in the loop. */
/* Accept this signal as the new signal if either it's longer
* than the previous one, or the previous one was unknown and
* this is decoded. */
if (thislen > app->signal_bestlen ||
(app->signal_decoded == false && decoded))
{
app->signal_info = *info;
app->signal_bestlen = thislen;
app->signal_decoded = decoded;
raw_samples_copy(DetectedSamples,copy);
raw_samples_center(DetectedSamples,i);
FURI_LOG_E(TAG, "Displayed sample updated (%d samples %lu us)",
(int)thislen, DetectedSamples->short_pulse_dur);
}
}
i += thislen ? thislen : 1;
}
raw_samples_free(copy);
free(info);
}
/* =============================================================================
* Decoding
*
* The following code will translates the raw singals as received by
* the CC1101 into logical signals: a bitmap of 0s and 1s sampled at
* the detected data clock interval.
*
* Then the converted signal is passed to the protocols decoders, that look
* for protocol-specific information. We stop at the first decoder that is
* able to decode the data, so protocols here should be registered in
* order of complexity and specificity, with the generic ones at the end.
* ===========================================================================*/
/* Set the 'bitpos' bit to value 'val', in the specified bitmap
* 'b' of len 'blen'.
* Out of range bits will silently be discarded. */
void bitmap_set(uint8_t *b, uint32_t blen, uint32_t bitpos, bool val) {
uint32_t byte = bitpos/8;
uint32_t bit = 7-(bitpos&7);
if (byte >= blen) return;
if (val)
b[byte] |= 1<<bit;
else
b[byte] &= ~(1<<bit);
}
/* Get the bit 'bitpos' of the bitmap 'b' of 'blen' bytes.
* Out of range bits return false (not bit set). */
bool bitmap_get(uint8_t *b, uint32_t blen, uint32_t bitpos) {
uint32_t byte = bitpos/8;
uint32_t bit = 7-(bitpos&7);
if (byte >= blen) return 0;
return (b[byte] & (1<<bit)) != 0;
}
/* We decode bits assuming the first bit we receive is the LSB
* (see bitmap_set/get functions). Many devices send data
* encoded in the reverse way. */
void bitmap_invert_bytes_bits(uint8_t *p, uint32_t len) {
for (uint32_t j = 0; j < len*8; j += 8) {
bool bits[8];
for (int i = 0; i < 8; i++) bits[i] = bitmap_get(p,len,j+i);
for (int i = 0; i < 8; i++) bitmap_set(p,len,j+i,bits[7-i]);
}
}
/* Return true if the specified sequence of bits, provided as a string in the
* form "11010110..." is found in the 'b' bitmap of 'blen' bits at 'bitpos'
* position. */
bool bitmap_match_bits(uint8_t *b, uint32_t blen, uint32_t bitpos, const char *bits) {
for (size_t j = 0; bits[j]; j++) {
bool expected = (bits[j] == '1') ? true : false;
if (bitmap_get(b,blen,bitpos+j) != expected) return false;
}
return true;
}
/* Search for the specified bit sequence (see bitmap_match_bits() for details)
* in the bitmap 'b' of 'blen' bytes, looking forward at most 'maxbits' ahead.
* Returns the offset (in bits) of the match, or BITMAP_SEEK_NOT_FOUND if not
* found.
*
* Note: there are better algorithms, such as Boyer-Moore. Here we hope that
* for the kind of patterns we search we'll have a lot of early stops so
* we use a vanilla approach. */
uint32_t bitmap_seek_bits(uint8_t *b, uint32_t blen, uint32_t startpos, uint32_t maxbits, const char *bits) {
uint32_t endpos = startpos+blen*8;
uint32_t end2 = startpos+maxbits;
if (end2 < endpos) endpos = end2;
for (uint32_t j = startpos; j < endpos; j++)
if (bitmap_match_bits(b,blen,j,bits)) return j;
return BITMAP_SEEK_NOT_FOUND;
}
/* Set the pattern 'pat' into the bitmap 'b' of max length 'blen' bytes.
* The pattern is given as a string of 0s and 1s characters, like "01101001".
* This function is useful in order to set the test vectors in the protocol
* decoders, to see if the decoding works regardless of the fact we are able
* to actually receive a given signal. */
void bitmap_set_pattern(uint8_t *b, uint32_t blen, const char *pat) {
uint32_t i = 0;
while(pat[i]) {
bitmap_set(b,blen,i,pat[i] == '1');
i++;
}
}
/* Take the raw signal and turn it into a sequence of bits inside the
* buffer 'b'. Note that such 0s and 1s are NOT the actual data in the
* signal, but is just a low level representation of the line code. Basically
* if the short pulse we find in the signal is 320us, we convert high and
* low levels in the raw sample in this way:
*
* If for instance we see a high level lasting ~600 us, we will add
* two 1s bit. If then the signal goes down for 330us, we will add one zero,
* and so forth. So for each period of high and low we find the closest
* multiple and set the relevant number of bits.
*
* In case of a short pulse of 320us detected, 320*2 is the closest to a
* high pulse of 600us, so 2 bits will be set.
*
* In other terms what this function does is sampling the signal at
* fixed 'rate' intervals.
*
* This representation makes it simple to decode the signal at a higher
* level later, translating it from Marshal coding or other line codes
* to the actual bits/bytes.
*
* The 'idx' argument marks the detected signal start index into the
* raw samples buffer. The 'count' tells the function how many raw
* samples to convert into bits. The function returns the number of
* bits set into the buffer 'b'. The 'rate' argument, in microseconds, is
* the detected short-pulse duration. We expect the line code to be
* meaningful when interpreted at multiples of 'rate'. */
uint32_t convert_signal_to_bits(uint8_t *b, uint32_t blen, RawSamplesBuffer *s, uint32_t idx, uint32_t count, uint32_t rate) {
if (rate == 0) return 0; /* We can't perform the conversion. */
uint32_t bitpos = 0;
for (uint32_t j = 0; j < count; j++) {
uint32_t dur;
bool level;
raw_samples_get(s, j+idx, &level, &dur);
uint32_t numbits = dur / rate; /* full bits that surely fit. */
uint32_t rest = dur % rate; /* How much we are left with. */
if (rest > rate/2) numbits++; /* There is another one. */
/* Limit how much a single sample can spawn. There are likely no
* protocols doing such long pulses when the rate is low. */
if (numbits > 1024) numbits = 1024;
if (0) /* Super verbose, so not under the DEBUG_MSG define. */
FURI_LOG_E(TAG, "%lu converted into %lu (%d) bits",
dur,numbits,(int)level);
/* If the signal is too short, let's claim it an interference
* and ignore it completely. */
if (numbits == 0) continue;
while(numbits--) bitmap_set(b,blen,bitpos++,level);
}
return bitpos;
}
/* This function converts the line code used to the final data representation.
* The representation is put inside 'buf', for up to 'buflen' bytes of total
* data. For instance in order to convert manchester I can use "10" and "01"
* as zero and one patterns. It is possible to use "?" inside patterns in
* order to skip certain bits. For instance certain devices encode data twice,
* with each bit encoded in manchester encoding and then in its reversed
* representation. In such a case I could use "10??" and "01??".
*
* The function returns the number of bits converted. It will stop as soon
* as it finds a pattern that does not match zero or one patterns, or when
* the end of the bitmap pointed by 'bits' is reached (the length is
* specified in bytes by the caller, via the 'len' parameters).
*
* The decoding starts at the specified offset (in bits) 'off'. */
uint32_t convert_from_line_code(uint8_t *buf, uint64_t buflen, uint8_t *bits, uint32_t len, uint32_t off, const char *zero_pattern, const char *one_pattern)
{
uint32_t decoded = 0; /* Number of bits extracted. */
len *= 8; /* Convert bytes to bits. */
while(off < len) {
bool bitval;
if (bitmap_match_bits(bits,len,off,zero_pattern)) {
bitval = false;
off += strlen(zero_pattern);
} else if (bitmap_match_bits(bits,len,off,one_pattern)) {
bitval = true;
off += strlen(one_pattern);
} else {
break;
}
bitmap_set(buf,buflen,decoded++,bitval);
if (decoded/8 == buflen) break; /* No space left on target buffer. */
}
return decoded;
}
/* Supported protocols go here, with the relevant implementation inside
* protocols/<name>.c */
extern ProtoViewDecoder Oregon2Decoder;
extern ProtoViewDecoder B4B1Decoder;
extern ProtoViewDecoder RenaultTPMSDecoder;
ProtoViewDecoder *Decoders[] = {
&Oregon2Decoder, /* Oregon sensors v2.1 protocol. */
&B4B1Decoder, /* PT, SC, ... 24 bits remotes. */
&RenaultTPMSDecoder, /* Renault TPMS. */
NULL
};
/* Reset the message info structure before passing it to the decoding
* functions. */
void initialize_msg_info(ProtoViewMsgInfo *i) {
memset(i,0,sizeof(ProtoViewMsgInfo));
}
/* This function is called when a new signal is detected. It converts it
* to a bitstream, and the calls the protocol specific functions for
* decoding. If the signal was decoded correctly by some protocol, true
* is returned. Otherwise false is returned. */
bool decode_signal(RawSamplesBuffer *s, uint64_t len, ProtoViewMsgInfo *info) {
uint32_t bitmap_bits_size = 4096*8;
uint32_t bitmap_size = bitmap_bits_size/8;
/* We call the decoders with an offset a few bits before the actual
* signal detected and for a len of a few bits after its end. */
uint32_t before_after_bits = 2;
uint8_t *bitmap = malloc(bitmap_size);
uint32_t bits = convert_signal_to_bits(bitmap,bitmap_size,s,-before_after_bits,len+before_after_bits*2,s->short_pulse_dur);
if (DEBUG_MSG) { /* Useful for debugging purposes. Don't remove. */
char *str = malloc(1024);
uint32_t j;
for (j = 0; j < bits && j < 1023; j++) {
str[j] = bitmap_get(bitmap,bitmap_size,j) ? '1' : '0';
}
str[j] = 0;
FURI_LOG_E(TAG, "%lu bits sampled: %s", bits, str);
free(str);
}
/* Try all the decoders available. */
int j = 0;
bool decoded = false;
while(Decoders[j]) {
uint32_t start_time = furi_get_tick();
decoded = Decoders[j]->decode(bitmap,bitmap_size,bits,info);
uint32_t delta = furi_get_tick() - start_time;
FURI_LOG_E(TAG, "Decoder %s took %lu ms",
Decoders[j]->name, (unsigned long)delta);
if (decoded) break;
j++;
}
if (!decoded) {
FURI_LOG_E(TAG, "No decoding possible");
} else {
FURI_LOG_E(TAG, "Decoded %s, raw=%s info=[%s,%s,%s]", info->name, info->raw, info->info1, info->info2, info->info3);
}
free(bitmap);
return decoded;
}
+30
View File
@@ -0,0 +1,30 @@
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
* See the LICENSE file for information about the license. */
#include "app.h"
void canvas_draw_str_with_border(Canvas* canvas, uint8_t x, uint8_t y, const char* str, Color text_color, Color border_color)
{
struct {
uint8_t x; uint8_t y;
} dir[8] = {
{-1,-1},
{0,-1},
{1,-1},
{1,0},
{1,1},
{0,1},
{-1,1},
{-1,0}
};
/* Rotate in all the directions writing the same string to create a
* border, then write the actual string in the other color in the
* middle. */
canvas_set_color(canvas, border_color);
for (int j = 0; j < 8; j++)
canvas_draw_str(canvas,x+dir[j].x,y+dir[j].y,str);
canvas_set_color(canvas, text_color);
canvas_draw_str(canvas,x,y,str);
canvas_set_color(canvas, ColorBlack);
}
@@ -0,0 +1,46 @@
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
* See the LICENSE file for information about the license. */
#include "app.h"
#include <cc1101.h>
/* Read directly from the G0 CC1101 pin, and draw a black or white
* dot depending on the level. */
void render_view_direct_sampling(Canvas *const canvas, ProtoViewApp *app) {
UNUSED(app);
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 128; x++) {
bool level = furi_hal_gpio_read(&gpio_cc1101_g0);
if (level) canvas_draw_dot(canvas,x,y);
/* Busy loop: this is a terrible approach as it blocks
* everything else, but for now it's the best we can do
* to obtain direct data with some spacing. */
uint32_t x = 500; while(x--);
}
}
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_with_border(canvas,40,60,"Direct sampling",
ColorWhite,ColorBlack);
}
/* Handle input */
void process_input_direct_sampling(ProtoViewApp *app, InputEvent input) {
UNUSED(app);
UNUSED(input);
}
/* Enter view. Stop the subghz thread to prevent access as we read
* the CC1101 data directly. */
void view_enter_direct_sampling(ProtoViewApp *app) {
if (app->txrx->txrx_state == TxRxStateRx) {
subghz_worker_stop(app->txrx->worker);
}
}
/* Exit view. Restore the subghz thread. */
void view_exit_direct_sampling(ProtoViewApp *app) {
if (app->txrx->txrx_state == TxRxStateRx) {
subghz_worker_start(app->txrx->worker);
}
}
@@ -0,0 +1,41 @@
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
* See the LICENSE file for information about the license. */
#include "app.h"
/* Renders the view with the detected message information. */
void render_view_info(Canvas *const canvas, ProtoViewApp *app) {
if (app->signal_decoded == false) {
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas, 30,36,"No signal decoded");
return;
}
/* Protocol name as title. */
canvas_set_font(canvas, FontPrimary);
uint8_t y = 8, lineheight = 10;
canvas_draw_str(canvas, 0, y, app->signal_info.name);
y += lineheight;
/* Info fields. */
char buf[128];
canvas_set_font(canvas, FontSecondary);
if (app->signal_info.raw[0]) {
snprintf(buf,sizeof(buf),"Raw: %s", app->signal_info.raw);
canvas_draw_str(canvas, 0, y, buf);
y += lineheight;
}
canvas_draw_str(canvas, 0, y, app->signal_info.info1);
y += lineheight;
canvas_draw_str(canvas, 0, y, app->signal_info.info2);
y += lineheight;
canvas_draw_str(canvas, 0, y, app->signal_info.info3);
y += lineheight;
}
/* Handle input for the settings view. */
void process_input_info(ProtoViewApp *app, InputEvent input) {
UNUSED(app);
UNUSED(input);
return;
}
@@ -0,0 +1,97 @@
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
* See the LICENSE file for information about the license. */
#include "app.h"
/* Render the received signal.
*
* The screen of the flipper is 128 x 64. Even using 4 pixels per line
* (where low level signal is one pixel high, high level is 4 pixels
* high) and 4 pixels of spacing between the different lines, we can
* plot comfortably 8 lines.
*
* The 'idx' argument is the first sample to render in the circular
* buffer. */
void render_signal(ProtoViewApp *app, Canvas *const canvas, RawSamplesBuffer *buf, uint32_t idx) {
canvas_set_color(canvas, ColorBlack);
int rows = 8;
uint32_t time_per_pixel = app->us_scale;
uint32_t start_idx = idx;
bool level = 0;
uint32_t dur = 0, sample_num = 0;
for (int row = 0; row < rows ; row++) {
for (int x = 0; x < 128; x++) {
int y = 3 + row*8;
if (dur < time_per_pixel/2) {
/* Get more data. */
raw_samples_get(buf, idx++, &level, &dur);
sample_num++;
}
canvas_draw_line(canvas, x,y,x,y-(level*3));
/* Write a small triangle under the last sample detected. */
if (app->signal_bestlen != 0 &&
sample_num+start_idx == app->signal_bestlen+1)
{
canvas_draw_dot(canvas,x,y+2);
canvas_draw_dot(canvas,x-1,y+3);
canvas_draw_dot(canvas,x,y+3);
canvas_draw_dot(canvas,x+1,y+3);
sample_num++; /* Make sure we don't mark the next, too. */
}
/* Remove from the current level duration the time we
* just plot. */
if (dur > time_per_pixel)
dur -= time_per_pixel;
else
dur = 0;
}
}
}
/* Raw pulses rendering. This is our default view. */
void render_view_raw_pulses(Canvas *const canvas, ProtoViewApp *app) {
/* Show signal. */
render_signal(app, canvas, DetectedSamples, app->signal_offset);
/* Show signal information. */
char buf[64];
snprintf(buf,sizeof(buf),"%luus",
(unsigned long)DetectedSamples->short_pulse_dur);
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_with_border(canvas, 97, 63, buf, ColorWhite, ColorBlack);
if (app->signal_decoded) {
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_with_border(canvas, 1, 61, app->signal_info.name, ColorWhite, ColorBlack);
}
}
/* Handle input for the raw pulses view. */
void process_input_raw_pulses(ProtoViewApp *app, InputEvent input) {
if (input.type == InputTypeRepeat) {
/* Handle panning of the signal window. Long pressing
* right will show successive samples, long pressing left
* previous samples. */
if (input.key == InputKeyRight) app->signal_offset++;
else if (input.key == InputKeyLeft) app->signal_offset--;
else if (input.key == InputKeyOk) {
app->signal_offset = 0;
app->us_scale = PROTOVIEW_RAW_VIEW_DEFAULT_SCALE;
}
} else if (input.type == InputTypeShort) {
if (input.key == InputKeyOk) {
/* Reset the current sample to capture the next. */
reset_current_signal(app);
} else if (input.key == InputKeyDown) {
/* Rescaling. The set becomes finer under 50us per pixel. */
uint32_t scale_step = app->us_scale >= 50 ? 50 : 10;
if (app->us_scale < 500) app->us_scale += scale_step;
} else if (input.key == InputKeyUp) {
uint32_t scale_step = app->us_scale > 50 ? 50 : 10;
if (app->us_scale > 10) app->us_scale -= scale_step;
}
}
}
@@ -0,0 +1,93 @@
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
* See the LICENSE file for information about the license. */
#include "app.h"
/* Renders a single view with frequency and modulation setting. However
* this are logically two different views, and only one of the settings
* will be highlighted. */
void render_view_settings(Canvas *const canvas, ProtoViewApp *app) {
canvas_set_font(canvas, FontPrimary);
if (app->current_view == ViewFrequencySettings)
canvas_draw_str_with_border(canvas,1,10,"Frequency",ColorWhite,ColorBlack);
else
canvas_draw_str(canvas,1,10,"Frequency");
if (app->current_view == ViewModulationSettings)
canvas_draw_str_with_border(canvas,70,10,"Modulation",ColorWhite,ColorBlack);
else
canvas_draw_str(canvas,70,10,"Modulation");
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas,10,61,"Use up and down to modify");
/* Show frequency. We can use big numbers font since it's just a number. */
if (app->current_view == ViewFrequencySettings) {
char buf[16];
snprintf(buf,sizeof(buf),"%.2f",(double)app->frequency/1000000);
canvas_set_font(canvas, FontBigNumbers);
canvas_draw_str(canvas, 30, 40, buf);
} else if (app->current_view == ViewModulationSettings) {
int current = app->modulation;
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 33, 39, ProtoViewModulations[current].name);
}
}
/* Handle input for the settings view. */
void process_input_settings(ProtoViewApp *app, InputEvent input) {
if (input.type == InputTypeLong && input.key == InputKeyOk) {
/* Long pressing to OK sets the default frequency and
* modulation. */
app->frequency = subghz_setting_get_default_frequency(app->setting);
app->modulation = 0;
} else if (input.type == InputTypePress &&
(input.key != InputKeyDown || input.key != InputKeyUp))
{
/* Handle up and down to change frequency or modulation. */
if (app->current_view == ViewFrequencySettings) {
size_t curidx = 0, i;
size_t count = subghz_setting_get_frequency_count(app->setting);
/* Scan the list of frequencies to check for the index of the
* currently set frequency. */
for(i = 0; i < count; i++) {
uint32_t freq = subghz_setting_get_frequency(app->setting,i);
if (freq == app->frequency) {
curidx = i;
break;
}
}
if (i == count) return; /* Should never happen. */
if (input.key == InputKeyUp) {
curidx = curidx == 0 ? count-1 : curidx-1;
} else if (input.key == InputKeyDown) {
curidx = (curidx+1) % count;
} else {
return;
}
app->frequency = subghz_setting_get_frequency(app->setting,curidx);
} else if (app->current_view == ViewModulationSettings) {
uint32_t count = 0;
uint32_t modid = app->modulation;
while(ProtoViewModulations[count].name != NULL) count++;
if (input.key == InputKeyUp) {
modid = modid == 0 ? count-1 : modid-1;
} else if (input.key == InputKeyDown) {
modid = (modid+1) % count;
} else {
return;
}
app->modulation = modid;
}
} else {
return;
}
/* Apply changes. */
FURI_LOG_E(TAG, "Setting view, setting frequency/modulation to %lu %s", app->frequency, ProtoViewModulations[app->modulation].name);
radio_rx_end(app);
radio_begin(app);
radio_rx(app);
}
@@ -1,6 +1,6 @@
App(
appid="RC2014_Coleco",
name="RC2014 ColecoVision",
name="[RC2014] ColecoVision",
apptype=FlipperAppType.EXTERNAL,
entry_point="coleco_app",
cdefines=["APP_COLECO"],
@@ -1,6 +1,6 @@
App(
appid="Signal_Generator",
name="Signal Generator",
name="[GPIO] Signal Generator",
apptype=FlipperAppType.EXTERNAL,
entry_point="signal_gen_app",
cdefines=["APP_SIGNAL_GEN"],
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 BlueChip
Copyright (c) 2022 Roman Shchekin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,9 @@
# flipper-zero-text-viewer
Text Viewer application for Flipper Zero!
A fork with a few changes from [QTRoS' hex viewer](https://github.com/QtRoS/flipper-zero-hex-viewer) to just display text without any hex byte representation
![Text Viewer app!](https://github.com/kyhwana/flipper-zero-hex-viewer/blob/master/textviewerflipper.PNG?raw=true)
[Link to FAP](https://github.com/kyhwana/latest_flipper_zero_apps/raw/main/text_viewer.fap)
@@ -0,0 +1,16 @@
App(
appid="text_viewer",
name="Text Viewer",
apptype=FlipperAppType.EXTERNAL,
entry_point="hex_viewer_app",
cdefines=["APP_TEXT_VIEWER"],
requires=[
"gui",
"dialogs",
],
stack_size=2 * 1024,
order=20,
fap_icon="icons/hex_10px.png",
fap_category="Misc",
fap_icon_assets="icons",
)
@@ -0,0 +1,282 @@
#include <furi.h>
#include <furi_hal.h>
#include <text_viewer_icons.h>
#include <gui/gui.h>
#include <gui/elements.h>
#include <dialogs/dialogs.h>
#include <storage/storage.h>
#include <stream/stream.h>
#include <stream/buffered_file_stream.h>
#include <toolbox/stream/file_stream.h>
#define TAG "TextViewer"
#define HEX_VIEWER_APP_PATH_FOLDER "/any"
#define HEX_VIEWER_APP_EXTENSION "*"
#define HEX_VIEWER_BYTES_PER_LINE 20u
#define HEX_VIEWER_LINES_ON_SCREEN 5u
#define HEX_VIEWER_BUF_SIZE (HEX_VIEWER_LINES_ON_SCREEN * HEX_VIEWER_BYTES_PER_LINE)
typedef struct {
uint8_t file_bytes[HEX_VIEWER_LINES_ON_SCREEN][HEX_VIEWER_BYTES_PER_LINE];
uint32_t file_offset;
uint32_t file_read_bytes;
uint32_t file_size;
Stream* stream;
bool mode; // Print address or content
} HexViewerModel;
typedef struct {
HexViewerModel* model;
FuriMutex** mutex;
FuriMessageQueue* input_queue;
ViewPort* view_port;
Gui* gui;
Storage* storage;
} HexViewer;
static void render_callback(Canvas* canvas, void* ctx) {
HexViewer* hex_viewer = ctx;
furi_check(furi_mutex_acquire(hex_viewer->mutex, FuriWaitForever) == FuriStatusOk);
canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
//elements_button_left(canvas, hex_viewer->model->mode ? "Addr" : "Text");
hex_viewer->model->mode = 1; //text mode
//elements_button_right(canvas, "Info");
int ROW_HEIGHT = 12;
int TOP_OFFSET = 10;
int LEFT_OFFSET = 3;
uint32_t line_count = hex_viewer->model->file_size / HEX_VIEWER_BYTES_PER_LINE;
if(hex_viewer->model->file_size % HEX_VIEWER_BYTES_PER_LINE != 0) line_count += 1;
uint32_t first_line_on_screen = hex_viewer->model->file_offset / HEX_VIEWER_BYTES_PER_LINE;
if(line_count > HEX_VIEWER_LINES_ON_SCREEN) {
uint8_t width = canvas_width(canvas);
elements_scrollbar_pos(
canvas,
width,
0,
ROW_HEIGHT * HEX_VIEWER_LINES_ON_SCREEN,
first_line_on_screen, // TODO
line_count - (HEX_VIEWER_LINES_ON_SCREEN - 1));
}
char temp_buf[32];
uint32_t row_iters = hex_viewer->model->file_read_bytes / HEX_VIEWER_BYTES_PER_LINE;
if(hex_viewer->model->file_read_bytes % HEX_VIEWER_BYTES_PER_LINE != 0) row_iters += 1;
for(uint32_t i = 0; i < row_iters; ++i) {
uint32_t bytes_left_per_row =
hex_viewer->model->file_read_bytes - i * HEX_VIEWER_BYTES_PER_LINE;
bytes_left_per_row = MIN(bytes_left_per_row, HEX_VIEWER_BYTES_PER_LINE);
if(hex_viewer->model->mode) {
memcpy(temp_buf, hex_viewer->model->file_bytes[i], bytes_left_per_row);
temp_buf[bytes_left_per_row] = '\0';
for(uint32_t j = 0; j < bytes_left_per_row; ++j)
if(!isprint((int)temp_buf[j])) temp_buf[j] = '.';
canvas_set_font(canvas, FontKeyboard);
canvas_draw_str(canvas, LEFT_OFFSET, TOP_OFFSET + i * ROW_HEIGHT, temp_buf);
} else {
uint32_t addr = hex_viewer->model->file_offset + i * HEX_VIEWER_BYTES_PER_LINE;
snprintf(temp_buf, 32, "%04lX", addr);
canvas_set_font(canvas, FontKeyboard);
canvas_draw_str(canvas, LEFT_OFFSET, TOP_OFFSET + i * ROW_HEIGHT, temp_buf);
}
}
furi_mutex_release(hex_viewer->mutex);
}
static void input_callback(InputEvent* input_event, void* ctx) {
HexViewer* hex_viewer = ctx;
if(input_event->type == InputTypeShort || input_event->type == InputTypeRepeat) {
furi_message_queue_put(hex_viewer->input_queue, input_event, 0);
}
}
static HexViewer* hex_viewer_alloc() {
HexViewer* instance = malloc(sizeof(HexViewer));
instance->model = malloc(sizeof(HexViewerModel));
memset(instance->model, 0x0, sizeof(HexViewerModel));
instance->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
instance->input_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
instance->view_port = view_port_alloc();
view_port_draw_callback_set(instance->view_port, render_callback, instance);
view_port_input_callback_set(instance->view_port, input_callback, instance);
instance->gui = furi_record_open(RECORD_GUI);
gui_add_view_port(instance->gui, instance->view_port, GuiLayerFullscreen);
instance->storage = furi_record_open(RECORD_STORAGE);
return instance;
}
static void hex_viewer_free(HexViewer* instance) {
furi_record_close(RECORD_STORAGE);
gui_remove_view_port(instance->gui, instance->view_port);
furi_record_close(RECORD_GUI);
view_port_free(instance->view_port);
furi_message_queue_free(instance->input_queue);
furi_mutex_free(instance->mutex);
if(instance->model->stream) buffered_file_stream_close(instance->model->stream);
free(instance->model);
free(instance);
}
static bool hex_viewer_open_file(HexViewer* hex_viewer, const char* file_path) {
furi_assert(hex_viewer);
furi_assert(file_path);
hex_viewer->model->stream = buffered_file_stream_alloc(hex_viewer->storage);
bool isOk = true;
do {
if(!buffered_file_stream_open(
hex_viewer->model->stream, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) {
FURI_LOG_E(TAG, "Unable to open stream: %s", file_path);
isOk = false;
break;
};
hex_viewer->model->file_size = stream_size(hex_viewer->model->stream);
} while(false);
return isOk;
}
static bool hex_viewer_read_file(HexViewer* hex_viewer) {
furi_assert(hex_viewer);
furi_assert(hex_viewer->model->stream);
furi_assert(hex_viewer->model->file_offset % HEX_VIEWER_BYTES_PER_LINE == 0);
memset(hex_viewer->model->file_bytes, 0x0, HEX_VIEWER_BUF_SIZE);
bool isOk = true;
do {
uint32_t offset = hex_viewer->model->file_offset;
if(!stream_seek(hex_viewer->model->stream, offset, true)) {
FURI_LOG_E(TAG, "Unable to seek stream");
isOk = false;
break;
}
hex_viewer->model->file_read_bytes = stream_read(
hex_viewer->model->stream,
(uint8_t*)hex_viewer->model->file_bytes,
HEX_VIEWER_BUF_SIZE);
} while(false);
return isOk;
}
int32_t hex_viewer_app(void* p) {
HexViewer* hex_viewer = hex_viewer_alloc();
FuriString* file_path;
file_path = furi_string_alloc();
do {
if(p && strlen(p)) {
furi_string_set(file_path, (const char*)p);
} else {
furi_string_set(file_path, HEX_VIEWER_APP_PATH_FOLDER);
DialogsFileBrowserOptions browser_options;
dialog_file_browser_set_basic_options(
&browser_options, HEX_VIEWER_APP_EXTENSION, &I_hex_10px);
browser_options.hide_ext = false;
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
bool res = dialog_file_browser_show(dialogs, file_path, file_path, &browser_options);
furi_record_close(RECORD_DIALOGS);
if(!res) {
FURI_LOG_I(TAG, "No file selected");
break;
}
}
FURI_LOG_I(TAG, "File selected: %s", furi_string_get_cstr(file_path));
if(!hex_viewer_open_file(hex_viewer, furi_string_get_cstr(file_path))) break;
hex_viewer_read_file(hex_viewer);
InputEvent input;
while(furi_message_queue_get(hex_viewer->input_queue, &input, FuriWaitForever) ==
FuriStatusOk) {
if(input.key == InputKeyBack) {
break;
} else if(input.key == InputKeyUp) {
furi_check(furi_mutex_acquire(hex_viewer->mutex, FuriWaitForever) == FuriStatusOk);
if(hex_viewer->model->file_offset > 0) {
hex_viewer->model->file_offset -= HEX_VIEWER_BYTES_PER_LINE;
if(!hex_viewer_read_file(hex_viewer)) break;
}
furi_mutex_release(hex_viewer->mutex);
} else if(input.key == InputKeyDown) {
furi_check(furi_mutex_acquire(hex_viewer->mutex, FuriWaitForever) == FuriStatusOk);
uint32_t last_byte_on_screen =
hex_viewer->model->file_offset + hex_viewer->model->file_read_bytes;
if(hex_viewer->model->file_size > last_byte_on_screen) {
hex_viewer->model->file_offset += HEX_VIEWER_BYTES_PER_LINE;
if(!hex_viewer_read_file(hex_viewer)) break;
}
furi_mutex_release(hex_viewer->mutex);
} else if(input.key == InputKeyLeft) {
furi_check(furi_mutex_acquire(hex_viewer->mutex, FuriWaitForever) == FuriStatusOk);
hex_viewer->model->mode = !hex_viewer->model->mode;
furi_mutex_release(hex_viewer->mutex);
} else if(input.key == InputKeyRight) {
FuriString* buffer;
buffer = furi_string_alloc();
furi_string_printf(
buffer,
"File path: %s\nFile size: %lu (0x%lX)",
furi_string_get_cstr(file_path),
hex_viewer->model->file_size,
hex_viewer->model->file_size);
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
DialogMessage* message = dialog_message_alloc();
dialog_message_set_header(message, "Text Viewer v1.1", 16, 2, AlignLeft, AlignTop);
dialog_message_set_icon(message, &I_hex_10px, 3, 2);
dialog_message_set_text(
message, furi_string_get_cstr(buffer), 3, 16, AlignLeft, AlignTop);
dialog_message_set_buttons(message, NULL, NULL, "Back");
dialog_message_show(dialogs, message);
furi_string_free(buffer);
dialog_message_free(message);
furi_record_close(RECORD_DIALOGS);
}
view_port_update(hex_viewer->view_port);
}
} while(false);
furi_string_free(file_path);
hex_viewer_free(hex_viewer);
return 0;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
App(
appid="unitemp",
name="Unitemp",
name="[GPIO] Unitemp",
apptype=FlipperAppType.EXTERNAL,
entry_point="unitemp_app",
cdefines=["UNITEMP_APP"],
Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

@@ -19,6 +19,7 @@
#include "unitemp_icons.h"
#include <assets_icons.h>
#include "../../../settings/desktop_settings/desktop_settings_app.h"
static View* view;
@@ -169,7 +170,14 @@ static void _draw_singleSensor(Canvas* canvas, Sensor* sensor, const uint8_t pos
}
static void _draw_view_noSensors(Canvas* canvas) {
canvas_draw_icon(canvas, 7, 17, &I_sherlok_53x45);
DesktopSettings* settings = malloc(sizeof(DesktopSettings));
DESKTOP_SETTINGS_LOAD(settings);
if (settings->sfw_mode) {
canvas_draw_icon(canvas, 7, 17, &I_sherlok_53x45_sfw);
}
else {
canvas_draw_icon(canvas, 7, 17, &I_sherlok_53x45);
}
//Рисование рамки
canvas_draw_rframe(canvas, 0, 0, 128, 63, 7);
canvas_draw_rframe(canvas, 0, 0, 128, 64, 7);
@@ -184,6 +192,7 @@ static void _draw_view_noSensors(Canvas* canvas) {
canvas_draw_str(canvas, x, y + 18, "press OK");
canvas_draw_icon(canvas, x + 37, y + 10, &I_Ok_btn_9x9);
free(settings);
}
static void _draw_view_sensorsList(Canvas* canvas) {
@@ -1,5 +1,8 @@
# Changelog
## 0.5
- Fix compatibility with Flipper Zero firmware 0.74.2
## 0.4
- Show active/inactive state in primary font (bold)
@@ -1,5 +1,7 @@
# qv. https://github.com/flipperdevices/flipperzero-firmware/blob/dev/documentation/AppManifests.md
App(
appid="USB_HID_Autofire",
appid="usb_hid_autofire",
name="USB HID Autofire",
apptype=FlipperAppType.EXTERNAL,
entry_point="usb_hid_autofire_app",
@@ -0,0 +1,56 @@
//
// Tools for USB HID Autofire
//
void strrev(char* arr, int start, int end) {
char temp;
if (start >= end)
return;
temp = *(arr + start);
*(arr + start) = *(arr + end);
*(arr + end) = temp;
start++;
end--;
strrev(arr, start, end);
}
char *itoa(int number, char *arr, int base)
{
int i = 0, r, negative = 0;
if (number == 0)
{
arr[i] = '0';
arr[i + 1] = '\0';
return arr;
}
if (number < 0 && base == 10)
{
number *= -1;
negative = 1;
}
while (number != 0)
{
r = number % base;
arr[i] = (r > 9) ? (r - 10) + 'a' : r + '0';
i++;
number /= base;
}
if (negative)
{
arr[i] = '-';
i++;
}
strrev(arr, 0, i - 1);
arr[i] = '\0';
return arr;
}
@@ -0,0 +1,7 @@
#ifndef FLIPPERZERO_FIRMWARE_TOOLS_H
#define FLIPPERZERO_FIRMWARE_TOOLS_H
void strrev(char *arr, int start, int end);
char *itoa(int number, char *arr, int base);
#endif //FLIPPERZERO_FIRMWARE_TOOLS_H
@@ -4,6 +4,7 @@
#include <gui/gui.h>
#include <input/input.h>
#include "version.h"
#include "tools.h"
// Uncomment to be able to make a screenshot
//#define USB_HID_AUTOFIRE_SCREENSHOT
@@ -25,7 +26,9 @@ uint32_t autofire_delay = 10;
static void usb_hid_autofire_render_callback(Canvas* canvas, void* ctx) {
UNUSED(ctx);
char autofire_delay_str[12];
//std::string pi = "pi is " + std::to_string(3.1415926);
itoa(autofire_delay, autofire_delay_str, 10);
//sprintf(autofire_delay_str, "%lu", autofire_delay);
canvas_clear(canvas);
@@ -85,19 +88,19 @@ int32_t usb_hid_autofire_app(void* p) {
}
switch(event.input.key) {
case InputKeyOk:
btn_left_autofire = !btn_left_autofire;
break;
case InputKeyLeft:
if(autofire_delay > 0) {
autofire_delay -= 10;
}
break;
case InputKeyRight:
autofire_delay += 10;
break;
default:
break;
case InputKeyOk:
btn_left_autofire = !btn_left_autofire;
break;
case InputKeyLeft:
if(autofire_delay > 0) {
autofire_delay -= 10;
}
break;
case InputKeyRight:
autofire_delay += 10;
break;
default:
break;
}
}
}
@@ -1 +1 @@
#define VERSION "0.4"
#define VERSION "0.5"
@@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto
@@ -1,14 +0,0 @@
App(
appid="USB_Midi",
name="USB Midi",
apptype=FlipperAppType.EXTERNAL,
entry_point="usb_midi_app",
requires=[
"gui",
],
stack_size=4 * 1024,
order=20,
fap_icon="usb_midi.png",
fap_category="Music",
# fap_icon_assets="icons",
)
@@ -1,3 +0,0 @@
#include <stdint.h>
#define SYSEX_BUFFER_LEN 16
@@ -1,144 +0,0 @@
#include "message.h"
/** Returns the data within the MidiEvent as a NoteOffEvent struct */
NoteOffEvent AsNoteOff(MidiEvent* event) {
NoteOffEvent m;
m.channel = event->channel;
m.note = event->data[0];
m.velocity = event->data[1];
return m;
}
/** Returns the data within the MidiEvent as a NoteOnEvent struct */
NoteOnEvent AsNoteOn(MidiEvent* event) {
NoteOnEvent m;
m.channel = event->channel;
m.note = event->data[0];
m.velocity = event->data[1];
return m;
}
/** Returns the data within the MidiEvent as a PolyphonicKeyPressureEvent struct */
PolyphonicKeyPressureEvent AsPolyphonicKeyPressure(MidiEvent* event) {
PolyphonicKeyPressureEvent m;
m.channel = event->channel;
m.note = event->data[0];
m.pressure = event->data[1];
return m;
}
/** Returns the data within the MidiEvent as a ControlChangeEvent struct.*/
ControlChangeEvent AsControlChange(MidiEvent* event) {
ControlChangeEvent m;
m.channel = event->channel;
m.control_number = event->data[0];
m.value = event->data[1];
return m;
}
/** Returns the data within the MidiEvent as a ProgramChangeEvent struct.*/
ProgramChangeEvent AsProgramChange(MidiEvent* event) {
ProgramChangeEvent m;
m.channel = event->channel;
m.program = event->data[0];
return m;
}
/** Returns the data within the MidiEvent as a ProgramChangeEvent struct.*/
ChannelPressureEvent AsChannelPressure(MidiEvent* event) {
ChannelPressureEvent m;
m.channel = event->channel;
m.pressure = event->data[0];
return m;
}
/** Returns the data within the MidiEvent as a PitchBendEvent struct.*/
PitchBendEvent AsPitchBend(MidiEvent* event) {
PitchBendEvent m;
m.channel = event->channel;
m.value = ((uint16_t)(event->data[1]) << 7) + (event->data[0] - 8192);
return m;
}
SystemExclusiveEvent AsSystemExclusive(MidiEvent* event) {
SystemExclusiveEvent m;
m.length = event->sysex_message_len;
for(int i = 0; i < SYSEX_BUFFER_LEN; i++) {
m.data[i] = 0;
if(i < m.length) {
m.data[i] = event->sysex_data[i];
}
}
return m;
}
MTCQuarterFrameEvent AsMTCQuarterFrame(MidiEvent* event) {
MTCQuarterFrameEvent m;
m.message_type = (event->data[0] & 0x70) >> 4;
m.value = (event->data[0]) & 0x0f;
return m;
}
SongPositionPointerEvent AsSongPositionPointer(MidiEvent* event) {
SongPositionPointerEvent m;
m.position = ((uint16_t)(event->data[1]) << 7) | (event->data[0]);
return m;
}
SongSelectEvent AsSongSelect(MidiEvent* event) {
SongSelectEvent m;
m.song = event->data[0];
return m;
}
AllSoundOffEvent AsAllSoundOff(MidiEvent* event) {
AllSoundOffEvent m;
m.channel = event->channel;
return m;
}
ResetAllControllersEvent AsResetAllControllers(MidiEvent* event) {
ResetAllControllersEvent m;
m.channel = event->channel;
m.value = event->data[1];
return m;
}
LocalControlEvent AsLocalControl(MidiEvent* event) {
LocalControlEvent m;
m.channel = event->channel;
m.local_control_off = (event->data[1] == 0);
m.local_control_on = (event->data[1] == 127);
return m;
}
AllNotesOffEvent AsAllNotesOff(MidiEvent* event) {
AllNotesOffEvent m;
m.channel = event->channel;
return m;
}
OmniModeOffEvent AsOmniModeOff(MidiEvent* event) {
OmniModeOffEvent m;
m.channel = event->channel;
return m;
}
OmniModeOnEvent AsOmniModeOn(MidiEvent* event) {
OmniModeOnEvent m;
m.channel = event->channel;
return m;
}
MonoModeOnEvent AsMonoModeOn(MidiEvent* event) {
MonoModeOnEvent m;
m.channel = event->channel;
m.num_channels = event->data[1];
return m;
}
PolyModeOnEvent AsPolyModeOn(MidiEvent* event) {
PolyModeOnEvent m;
m.channel = event->channel;
return m;
}
@@ -1,251 +0,0 @@
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "config.h"
typedef enum {
NoteOff, /**< & */
NoteOn, /**< & */
PolyphonicKeyPressure, /**< & */
ControlChange, /**< & */
ProgramChange, /**< & */
ChannelPressure, /**< & */
PitchBend, /**< & */
SystemCommon, /**< & */
SystemRealTime, /**< & */
ChannelMode, /**< & */
MessageLast, /**< & */
} MidiMessageType;
typedef enum {
SystemExclusive, /**< & */
MTCQuarterFrame, /**< & */
SongPositionPointer, /**< & */
SongSelect, /**< & */
SCUndefined0, /**< & */
SCUndefined1, /**< & */
TuneRequest, /**< & */
SysExEnd, /**< & */
SystemCommonLast, /**< & */
} SystemCommonType;
typedef enum {
TimingClock, /**< & */
SRTUndefined0, /**< & */
Start, /**< & */
Continue, /**< & */
Stop, /**< & */
SRTUndefined1, /**< & */
ActiveSensing, /**< & */
Reset, /**< & */
SystemRealTimeLast, /**< & */
} SystemRealTimeType;
typedef enum {
AllSoundOff, /**< & */
ResetAllControllers, /**< & */
LocalControl, /**< & */
AllNotesOff, /**< & */
OmniModeOff, /**< & */
OmniModeOn, /**< & */
MonoModeOn, /**< & */
PolyModeOn, /**< & */
ChannelModeLast, /**< & */
} ChannelModeType;
/** Struct containing note, and velocity data for a given channel.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
uint8_t note; /**< & */
uint8_t velocity; /**< & */
} NoteOffEvent;
/** Struct containing note, and velocity data for a given channel.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
uint8_t note; /**< & */
uint8_t velocity; /**< & */
} NoteOnEvent;
/** Struct containing note, and pressure data for a given channel.
Can be made from MidiEvent
*/
typedef struct {
int channel;
uint8_t note;
uint8_t pressure;
} PolyphonicKeyPressureEvent;
/** Struct containing control number, and value for a given channel.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
uint8_t control_number; /**< & */
uint8_t value; /**< & */
} ControlChangeEvent;
/** Struct containing new program number, for a given channel.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
uint8_t program; /**< & */
} ProgramChangeEvent;
/** Struct containing pressure (aftertouch), for a given channel.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
uint8_t pressure; /**< & */
} ChannelPressureEvent;
/** Struct containing pitch bend value for a given channel.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
int16_t value; /**< & */
} PitchBendEvent;
/** Struct containing sysex data.
Can be made from MidiEvent
*/
typedef struct {
int length;
uint8_t data[SYSEX_BUFFER_LEN]; /**< & */
} SystemExclusiveEvent;
/** Struct containing QuarterFrame data.
Can be made from MidiEvent
*/
typedef struct {
uint8_t message_type; /**< & */
uint8_t value; /**< & */
} MTCQuarterFrameEvent;
/** Struct containing song position data.
Can be made from MidiEvent
*/
typedef struct {
uint16_t position; /**< & */
} SongPositionPointerEvent;
/** Struct containing song select data.
Can be made from MidiEvent
*/
typedef struct {
uint8_t song; /**< & */
} SongSelectEvent;
/** Struct containing sound off data.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
} AllSoundOffEvent;
/** Struct containing ResetAllControllersEvent data.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
uint8_t value; /**< & */
} ResetAllControllersEvent;
/** Struct containing LocalControlEvent data.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
bool local_control_off; /**< & */
bool local_control_on; /**< & */
} LocalControlEvent;
/** Struct containing AllNotesOffEvent data.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
} AllNotesOffEvent;
/** Struct containing OmniModeOffEvent data.
* Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
} OmniModeOffEvent;
/** Struct containing OmniModeOnEvent data.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
} OmniModeOnEvent;
/** Struct containing MonoModeOnEvent data.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
uint8_t num_channels; /**< & */
} MonoModeOnEvent;
/** Struct containing PolyModeOnEvent data.
Can be made from MidiEvent
*/
typedef struct {
int channel; /**< & */
} PolyModeOnEvent;
/** Simple MidiEvent with message type, channel, and data[2] members.
*/
typedef struct {
MidiMessageType type;
int channel;
uint8_t data[2];
uint8_t sysex_data[SYSEX_BUFFER_LEN];
uint8_t sysex_message_len;
SystemCommonType sc_type;
SystemRealTimeType srt_type;
ChannelModeType cm_type;
} MidiEvent;
/** Returns the data within the MidiEvent as a NoteOffEvent struct */
NoteOffEvent AsNoteOff(MidiEvent* event);
/** Returns the data within the MidiEvent as a NoteOnEvent struct */
NoteOnEvent AsNoteOn(MidiEvent* event);
/** Returns the data within the MidiEvent as a PolyphonicKeyPressureEvent struct */
PolyphonicKeyPressureEvent AsPolyphonicKeyPressure(MidiEvent* event);
/** Returns the data within the MidiEvent as a ControlChangeEvent struct.*/
ControlChangeEvent AsControlChange(MidiEvent* event);
/** Returns the data within the MidiEvent as a ProgramChangeEvent struct.*/
ProgramChangeEvent AsProgramChange(MidiEvent* event);
/** Returns the data within the MidiEvent as a ProgramChangeEvent struct.*/
ChannelPressureEvent AsChannelPressure(MidiEvent* event);
/** Returns the data within the MidiEvent as a PitchBendEvent struct.*/
PitchBendEvent AsPitchBend(MidiEvent* event);
SystemExclusiveEvent AsSystemExclusive(MidiEvent* event);
MTCQuarterFrameEvent AsMTCQuarterFrame(MidiEvent* event);
SongPositionPointerEvent AsSongPositionPointer(MidiEvent* event);
SongSelectEvent AsSongSelect(MidiEvent* event);
AllSoundOffEvent AsAllSoundOff(MidiEvent* event);
ResetAllControllersEvent AsResetAllControllers(MidiEvent* event);
LocalControlEvent AsLocalControl(MidiEvent* event);
AllNotesOffEvent AsAllNotesOff(MidiEvent* event);
OmniModeOffEvent AsOmniModeOff(MidiEvent* event);
OmniModeOnEvent AsOmniModeOn(MidiEvent* event);
MonoModeOnEvent AsMonoModeOn(MidiEvent* event);
PolyModeOnEvent AsPolyModeOn(MidiEvent* event);
-149
View File
@@ -1,149 +0,0 @@
#include <stdlib.h>
#include "parser.h"
typedef enum {
ParserEmpty,
ParserHasStatus,
ParserHasData0,
ParserSysEx,
} ParserState;
const uint8_t kStatusByteMask = 0x80;
const uint8_t kMessageMask = 0x70;
const uint8_t kDataByteMask = 0x7F;
const uint8_t kSystemCommonMask = 0xF0;
const uint8_t kChannelMask = 0x0F;
const uint8_t kRealTimeMask = 0xF8;
const uint8_t kSystemRealTimeMask = 0x07;
struct MidiParser {
MidiMessageType status;
ParserState state;
MidiEvent incoming_message;
};
MidiParser* midi_parser_alloc(void) {
MidiParser* parser = malloc(sizeof(MidiParser));
parser->incoming_message.type = MessageLast;
parser->state = ParserEmpty;
return parser;
}
void midi_parser_free(MidiParser* parser) {
free(parser);
}
bool midi_parser_parse(MidiParser* parser, uint8_t byte) {
bool parsed = false;
MidiEvent* event = &parser->incoming_message;
switch(parser->state) {
case ParserEmpty:
// check byte for valid Status Byte
if(byte & kStatusByteMask) {
// Get MessageType, and Channel
event->channel = byte & kChannelMask;
event->type = (MidiMessageType)((byte & kMessageMask) >> 4);
// Validate, and move on.
if(event->type < MessageLast) {
parser->state = ParserHasStatus;
// Mark this status byte as running_status
parser->status = event->type;
if(parser->status == SystemCommon) {
event->channel = 0;
//system real time = 1111 1xxx
if(byte & 0x08) {
event->type = SystemRealTime;
parser->status = SystemRealTime;
event->srt_type = (SystemRealTimeType)(byte & kSystemRealTimeMask);
//short circuit to start
parser->state = ParserEmpty;
//queue_.push(incoming_message_);
parsed = true;
}
//system common
else {
event->sc_type = (SystemCommonType)(byte & 0x07);
//sysex
if(event->sc_type == SystemExclusive) {
parser->state = ParserSysEx;
event->sysex_message_len = 0;
}
//short circuit
else if(event->sc_type > SongSelect) {
parser->state = ParserEmpty;
//queue_.push(incoming_message_);
parsed = true;
}
}
}
}
// Else we'll keep waiting for a valid incoming status byte
} else {
// Handle as running status
event->type = parser->status;
event->data[0] = byte & kDataByteMask;
parser->state = ParserHasData0;
}
break;
case ParserHasStatus:
if((byte & kStatusByteMask) == 0) {
event->data[0] = byte & kDataByteMask;
if(parser->status == ChannelPressure || parser->status == ProgramChange ||
event->sc_type == MTCQuarterFrame || event->sc_type == SongSelect) {
//these are just one data byte, so we short circuit back to start
parser->state = ParserEmpty;
//queue_.push(incoming_message_);
parsed = true;
} else {
parser->state = ParserHasData0;
}
//ChannelModeMessages (reserved Control Changes)
if(parser->status == ControlChange && event->data[0] > 119) {
event->type = ChannelMode;
parser->status = ChannelMode;
event->cm_type = (ChannelModeType)(event->data[0] - 120);
}
} else {
// invalid message go back to start ;p
parser->state = ParserEmpty;
}
break;
case ParserHasData0:
if((byte & kStatusByteMask) == 0) {
event->data[1] = byte & kDataByteMask;
// At this point the message is valid, and we can add this MidiEvent to the queue
//queue_.push(incoming_message_);
parsed = true;
}
// Regardless, of whether the data was valid or not we go back to empty
// because either the message is queued for handling or its not.
parser->state = ParserEmpty;
break;
case ParserSysEx:
// end of sysex
if(byte == 0xf7) {
parser->state = ParserEmpty;
//queue_.push(incoming_message_);
parsed = true;
} else {
if(event->sysex_message_len < SYSEX_BUFFER_LEN) {
event->sysex_data[event->sysex_message_len] = byte;
event->sysex_message_len++;
}
}
break;
default:
break;
}
return parsed;
}
MidiEvent* midi_parser_get_message(MidiParser* parser) {
return &parser->incoming_message;
}

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