mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-05-13 03:28:36 -07:00
24
applications/plugins/protoview/LICENSE
Normal file
24
applications/plugins/protoview/LICENSE
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Copyright (c) 2022-2023 Salvatore Sanfilippo <antirez at gmail dot com>
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
103
applications/plugins/protoview/README.md
Normal file
103
applications/plugins/protoview/README.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
ProtoView is a digital signal detection and visualization tool for the
|
||||||
|
[Flipper Zero](https://flipperzero.one/). The Flipper is able to identify
|
||||||
|
a great deal of RF protocols, however when the exact protocol is not
|
||||||
|
implemented (and there are many proprietary ones, such as the ones of
|
||||||
|
the car keys), the curious person is left wondering what the device is
|
||||||
|
sending at all. Using ProtoView she or he can visualize the high and low pulses
|
||||||
|
like in the example image below (showing a Volkswagen key in 2FSK):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This is often enough to make an initial idea about the encoding used
|
||||||
|
and if the selected modulation is correct.
|
||||||
|
|
||||||
|
The secondary goal of ProtoView is to provide a somewhat-documented application
|
||||||
|
for the Flipper (even if ProtoView is a pretty atypical application: doesn't make use of the standard widgets and other abstractions provded by the framework).
|
||||||
|
Many apps dealing with the *subghz subsystem* (the Flipper
|
||||||
|
abstraction to work with the [CC1101 chip](https://www.ti.com/product/CC1101))
|
||||||
|
tend to be complicated and completely undocumented. This is unfortunately
|
||||||
|
true for the firmware of the device itself. It's a shame because especially
|
||||||
|
in the case of code that talks with hardware peripherals there are tons
|
||||||
|
of assumptions and hard-gained lessons that can [only be captured by comments and are in the code only implicitly](http://antirez.com/news/124).
|
||||||
|
|
||||||
|
However, the Flipper firmware source code is well written even if it
|
||||||
|
lacks comments and documentation, so it is possible to make some ideas of
|
||||||
|
how things work just grepping inside.
|
||||||
|
|
||||||
|
# Detection algorithm
|
||||||
|
|
||||||
|
In order to show unknown signals, the application attempts to understand if
|
||||||
|
the samples obtained by the Flipper API (a series of pulses that are high
|
||||||
|
or low, and with different duration in microseconds) look like belonging to
|
||||||
|
a legitimate signal, and aren't just noise.
|
||||||
|
|
||||||
|
We can't make assumptions about
|
||||||
|
the encoding and the data rate of the communication, so we use a simple
|
||||||
|
but relatively effective algorithm. As we check the signal, we try to detect
|
||||||
|
long parts of it that are composed of pulses roughly classifiable into
|
||||||
|
a maximum of three different classes of lengths, plus or minus 10%. Most
|
||||||
|
encodings are somewhat self-clocked, so they tend to have just two or
|
||||||
|
three classes of pulse lengths.
|
||||||
|
|
||||||
|
However often pulses of the same theoretical
|
||||||
|
length have slightly different lenghts in the case of high and low level
|
||||||
|
(RF on or off), so we classify them separately for robustness.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
The application shows the longest coherent signal detected so far.
|
||||||
|
|
||||||
|
* The OK button resets the current signal.
|
||||||
|
* The UP and DOWN buttons change the scale. Default is 100us per pixel.
|
||||||
|
* The LEFT and RIGHT buttons switch to settings.
|
||||||
|
|
||||||
|
Under the detected sequence, you will see a small triangle marking a
|
||||||
|
specific sample. This mark means that the sequence looked coherent up
|
||||||
|
to that point, and starting from there it could be just noise.
|
||||||
|
|
||||||
|
In the bottom-right corner the application displays an amount of time
|
||||||
|
in microseconds. This is the average length of the shortest pulse length
|
||||||
|
detected among the three classes. Usually the *data rate* of the protocol
|
||||||
|
is something like `1000000/this-number*2`, but it depends on the encoding
|
||||||
|
and could actually be `1000000/this-number*N` with `N > 2` (here 1000000
|
||||||
|
is the number of microseconds in one second, and N is the number of clock
|
||||||
|
cycles needed to represent a bit).
|
||||||
|
|
||||||
|
Things to investigate:
|
||||||
|
|
||||||
|
* Many cheap remotes (gate openers, remotes, ...) are on the 433.92Mhz or nearby and use OOK modulation.
|
||||||
|
* Weather stations are often too in the 433.92Mhz OOK.
|
||||||
|
* For car keys, try 443.92 OOK650 and 868.35 Mhz in OOK or 2FSK.
|
||||||
|
|
||||||
|
# Installing the app from source
|
||||||
|
|
||||||
|
* Download the Flipper Zero dev kit and build it:
|
||||||
|
```
|
||||||
|
mkdir -p ~/flipperZero/official/
|
||||||
|
cd ~/flipperZero/official/
|
||||||
|
git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git ./
|
||||||
|
./fbt
|
||||||
|
```
|
||||||
|
* Copy this application folder in `official/application_user`.
|
||||||
|
* Connect your Flipper via USB.
|
||||||
|
* Build and install with: `./fbt launch_app APPSRC=protoview`.
|
||||||
|
|
||||||
|
# Installing the binary file (no build needed)
|
||||||
|
|
||||||
|
Drop the `protoview.fap` file you can find in the `binaries` folder into the
|
||||||
|
following Flipper Zero location:
|
||||||
|
|
||||||
|
/ext/apps/Tools
|
||||||
|
|
||||||
|
The `ext` part means that we are in the SD card. So if you don't want
|
||||||
|
to use the Android (or other) application to upload the file,
|
||||||
|
you can just take out the SD card, insert it in your computer,
|
||||||
|
copy the fine into `apps/Tools`, and that's it.
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
The code is released under the BSD license.
|
||||||
|
|
||||||
|
# Disclaimer
|
||||||
|
|
||||||
|
This application is only provided as an educational tool. The author is not liable in case the application is used to reverse engineer protocols protected by IP or for any other illegal purpose.
|
||||||
16
applications/plugins/protoview/TODO
Normal file
16
applications/plugins/protoview/TODO
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
Features
|
||||||
|
========
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
Screens sequence (user can navigate with <- and ->):
|
||||||
|
|
||||||
|
(default)
|
||||||
|
[settings] <> [freq] <> [pulses view] <> [raw square view] <> [signal info]
|
||||||
515
applications/plugins/protoview/app.c
Normal file
515
applications/plugins/protoview/app.c
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
/* 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"
|
||||||
|
|
||||||
|
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. */
|
||||||
|
static void render_callback(Canvas *const canvas, void *ctx) {
|
||||||
|
ProtoViewApp *app = ctx;
|
||||||
|
|
||||||
|
/* Clear screen. */
|
||||||
|
canvas_set_color(canvas, ColorWhite);
|
||||||
|
canvas_draw_box(canvas, 0, 0, 127, 63);
|
||||||
|
canvas_set_color(canvas, ColorBlack);
|
||||||
|
canvas_set_font(canvas, FontPrimary);
|
||||||
|
|
||||||
|
/* Call who is in charge right now. */
|
||||||
|
switch(app->current_view) {
|
||||||
|
case ViewRawPulses: render_view_raw_pulses(canvas,app); break;
|
||||||
|
case ViewFrequencySettings:
|
||||||
|
case ViewModulationSettings:
|
||||||
|
render_view_settings(canvas,app); break;
|
||||||
|
case ViewLast: furi_crash(TAG " ViewLast selected"); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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. */
|
||||||
|
static void input_callback(InputEvent* input_event, void* ctx)
|
||||||
|
{
|
||||||
|
ProtoViewApp *app = ctx;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allocate the application state and initialize a number of stuff.
|
||||||
|
* This is called in the entry point to create the application state. */
|
||||||
|
ProtoViewApp* protoview_app_alloc() {
|
||||||
|
ProtoViewApp *app = malloc(sizeof(ProtoViewApp));
|
||||||
|
|
||||||
|
// Init shared data structures
|
||||||
|
RawSamples = raw_samples_alloc();
|
||||||
|
DetectedSamples = raw_samples_alloc();
|
||||||
|
|
||||||
|
//init setting
|
||||||
|
app->setting = subghz_setting_alloc();
|
||||||
|
subghz_setting_load(app->setting, EXT_PATH("protoview/settings.txt"));
|
||||||
|
|
||||||
|
// GUI
|
||||||
|
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->current_view = ViewRawPulses;
|
||||||
|
|
||||||
|
// Signal found and visualization defaults
|
||||||
|
app->signal_bestlen = 0;
|
||||||
|
app->us_scale = 100;
|
||||||
|
|
||||||
|
//init Worker & Protocol
|
||||||
|
app->txrx = malloc(sizeof(ProtoViewTxRx));
|
||||||
|
|
||||||
|
/* Setup rx worker and environment. */
|
||||||
|
app->txrx->worker = subghz_worker_alloc();
|
||||||
|
app->txrx->environment = subghz_environment_alloc();
|
||||||
|
subghz_environment_set_protocol_registry(
|
||||||
|
app->txrx->environment, (void*)&protoview_protocol_registry);
|
||||||
|
app->txrx->receiver = subghz_receiver_alloc_init(app->txrx->environment);
|
||||||
|
|
||||||
|
subghz_receiver_set_filter(app->txrx->receiver, SubGhzProtocolFlag_Decodable);
|
||||||
|
subghz_worker_set_overrun_callback(
|
||||||
|
app->txrx->worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset);
|
||||||
|
subghz_worker_set_pair_callback(
|
||||||
|
app->txrx->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode);
|
||||||
|
subghz_worker_set_context(app->txrx->worker, app->txrx->receiver);
|
||||||
|
|
||||||
|
app->frequency = subghz_setting_get_default_frequency(app->setting);
|
||||||
|
app->modulation = 0; /* Defaults to ProtoViewModulations[0]. */
|
||||||
|
|
||||||
|
furi_hal_power_suppress_charge_enter();
|
||||||
|
app->running = 1;
|
||||||
|
|
||||||
|
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 protoview_app_free(ProtoViewApp *app) {
|
||||||
|
furi_assert(app);
|
||||||
|
|
||||||
|
// Put CC1101 on sleep.
|
||||||
|
radio_sleep(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;
|
||||||
|
|
||||||
|
// Frequency setting.
|
||||||
|
subghz_setting_free(app->setting);
|
||||||
|
|
||||||
|
// Worker stuff.
|
||||||
|
subghz_receiver_free(app->txrx->receiver);
|
||||||
|
subghz_environment_free(app->txrx->environment);
|
||||||
|
subghz_worker_free(app->txrx->worker);
|
||||||
|
free(app->txrx);
|
||||||
|
|
||||||
|
// Raw samples buffers.
|
||||||
|
raw_samples_free(RawSamples);
|
||||||
|
raw_samples_free(DetectedSamples);
|
||||||
|
furi_hal_power_suppress_charge_exit();
|
||||||
|
|
||||||
|
free(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Called periodically. Do signal processing here. Data we process here
|
||||||
|
* will be later displayed by the render callback. The side effect of this
|
||||||
|
* function is to scan for signals and set DetectedSamples. */
|
||||||
|
static void timer_callback(void *ctx) {
|
||||||
|
ProtoViewApp *app = 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();
|
||||||
|
|
||||||
|
/* Create a timer. We do data analysis in the callback. */
|
||||||
|
FuriTimer *timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, app);
|
||||||
|
furi_timer_start(timer, furi_kernel_get_tick_frequency() / 4);
|
||||||
|
|
||||||
|
/* Start listening to signals immediately. */
|
||||||
|
radio_begin(app);
|
||||||
|
radio_rx(app);
|
||||||
|
|
||||||
|
/* 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. The timeout is 100 milliseconds, so if not input is received
|
||||||
|
* before such time, we exit the queue_get() function and call
|
||||||
|
* view_port_update() in order to refresh our screen content. */
|
||||||
|
InputEvent input;
|
||||||
|
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);
|
||||||
|
|
||||||
|
/* Handle navigation here. Then handle view-specific inputs
|
||||||
|
* in the view specific handling function. */
|
||||||
|
if (input.key == InputKeyBack) {
|
||||||
|
/* Exit the app. */
|
||||||
|
app->running = 0;
|
||||||
|
} else if (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) {
|
||||||
|
/* Go to the previous view. */
|
||||||
|
if (app->current_view == 0)
|
||||||
|
app->current_view = ViewLast-1;
|
||||||
|
else
|
||||||
|
app->current_view--;
|
||||||
|
} else {
|
||||||
|
switch(app->current_view) {
|
||||||
|
case ViewRawPulses:
|
||||||
|
process_input_raw_pulses(app,input);
|
||||||
|
break;
|
||||||
|
case ViewFrequencySettings:
|
||||||
|
case ViewModulationSettings:
|
||||||
|
process_input_settings(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");
|
||||||
|
}
|
||||||
|
view_port_update(app->view_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* App no longer running. Shut down and free. */
|
||||||
|
if (app->txrx->txrx_state == TxRxStateRx) {
|
||||||
|
FURI_LOG_E(TAG, "Putting CC1101 to sleep before exiting.");
|
||||||
|
radio_rx_end(app);
|
||||||
|
radio_sleep(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
furi_timer_free(timer);
|
||||||
|
protoview_app_free(app);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
82
applications/plugins/protoview/app.h
Normal file
82
applications/plugins/protoview/app.h
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||||
|
* See the LICENSE file for information about the license. */
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gui/gui.h>
|
||||||
|
#include <gui/view_dispatcher.h>
|
||||||
|
#include <gui/scene_manager.h>
|
||||||
|
#include <gui/modules/submenu.h>
|
||||||
|
#include <gui/modules/variable_item_list.h>
|
||||||
|
#include <gui/modules/widget.h>
|
||||||
|
#include <notification/notification_messages.h>
|
||||||
|
#include <lib/subghz/subghz_setting.h>
|
||||||
|
#include <lib/subghz/subghz_worker.h>
|
||||||
|
#include <lib/subghz/receiver.h>
|
||||||
|
#include <lib/subghz/transmitter.h>
|
||||||
|
#include <lib/subghz/registry.h>
|
||||||
|
|
||||||
|
#define TAG "ProtoView"
|
||||||
|
|
||||||
|
typedef struct ProtoViewApp ProtoViewApp;
|
||||||
|
|
||||||
|
/* Subghz system state */
|
||||||
|
typedef enum {
|
||||||
|
TxRxStateIDLE,
|
||||||
|
TxRxStateRx,
|
||||||
|
TxRxStateSleep,
|
||||||
|
} TxRxState;
|
||||||
|
|
||||||
|
/* Currently active view. */
|
||||||
|
typedef enum {
|
||||||
|
ViewRawPulses,
|
||||||
|
ViewFrequencySettings,
|
||||||
|
ViewModulationSettings,
|
||||||
|
ViewLast, /* Just a sentinel to wrap around. */
|
||||||
|
} ProtoViewCurrentView;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
FuriHalSubGhzPreset preset;
|
||||||
|
} ProtoViewModulation;
|
||||||
|
|
||||||
|
extern ProtoViewModulation ProtoViewModulations[]; /* In app_subghz.c */
|
||||||
|
|
||||||
|
/* This is the context of our subghz worker and associated thread.
|
||||||
|
* It receives data and we get our protocol "feed" callback called
|
||||||
|
* with the level (1 or 0) and duration. */
|
||||||
|
struct ProtoViewTxRx {
|
||||||
|
SubGhzWorker* worker; /* Our background worker. */
|
||||||
|
SubGhzEnvironment* environment;
|
||||||
|
SubGhzReceiver* receiver;
|
||||||
|
TxRxState txrx_state; /* Receiving, idle or sleeping? */
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct ProtoViewTxRx ProtoViewTxRx;
|
||||||
|
|
||||||
|
struct ProtoViewApp {
|
||||||
|
/* GUI */
|
||||||
|
Gui *gui;
|
||||||
|
ViewPort *view_port; /* We just use a raw viewport and we render
|
||||||
|
everything into the low level canvas. */
|
||||||
|
ProtoViewCurrentView current_view; /* Active view ID. */
|
||||||
|
FuriMessageQueue *event_queue; /* Keypress events go here. */
|
||||||
|
|
||||||
|
/* Radio related. */
|
||||||
|
ProtoViewTxRx *txrx; /* Radio state. */
|
||||||
|
SubGhzSetting *setting; /* A list of valid frequencies. */
|
||||||
|
|
||||||
|
/* Application state and config. */
|
||||||
|
int running; /* Once false exists the app. */
|
||||||
|
uint32_t signal_bestlen; /* Longest coherent signal observed so far. */
|
||||||
|
uint32_t us_scale; /* microseconds per pixel. */
|
||||||
|
uint32_t frequency; /* Current frequency. */
|
||||||
|
uint8_t modulation; /* Current modulation ID, array index in the
|
||||||
|
ProtoViewModulations table. */
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
67
applications/plugins/protoview/app_buffer.c
Normal file
67
applications/plugins/protoview/app_buffer.c
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||||
|
* See the LICENSE file for information about the license. */
|
||||||
|
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <furi/core/string.h>
|
||||||
|
#include <furi.h>
|
||||||
|
#include <furi_hal.h>
|
||||||
|
#include "app_buffer.h"
|
||||||
|
|
||||||
|
/* Allocate and initialize a samples buffer. */
|
||||||
|
RawSamplesBuffer *raw_samples_alloc(void) {
|
||||||
|
RawSamplesBuffer *buf = malloc(sizeof(*buf));
|
||||||
|
buf->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||||
|
raw_samples_reset(buf);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Free a sample buffer. Should be called when the mutex is released. */
|
||||||
|
void raw_samples_free(RawSamplesBuffer *s) {
|
||||||
|
furi_mutex_free(s->mutex);
|
||||||
|
free(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This just set all the samples to zero and also resets the internal
|
||||||
|
* index. There is no need to call it after raw_samples_alloc(), but only
|
||||||
|
* when one wants to reset the whole buffer of samples. */
|
||||||
|
void raw_samples_reset(RawSamplesBuffer *s) {
|
||||||
|
furi_mutex_acquire(s->mutex,FuriWaitForever);
|
||||||
|
s->total = RAW_SAMPLES_NUM;
|
||||||
|
s->idx = 0;
|
||||||
|
s->short_pulse_dur = 0;
|
||||||
|
memset(s->level,0,sizeof(s->level));
|
||||||
|
memset(s->dur,0,sizeof(s->dur));
|
||||||
|
furi_mutex_release(s->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
s->level[s->idx] = level;
|
||||||
|
s->dur[s->idx] = dur;
|
||||||
|
s->idx = (s->idx+1) % RAW_SAMPLES_NUM;
|
||||||
|
furi_mutex_release(s->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the sample from the buffer. It is possible to use out of range indexes
|
||||||
|
* as 'idx' because the modulo operation will rewind back from the start. */
|
||||||
|
void raw_samples_get(RawSamplesBuffer *s, uint32_t idx, bool *level, uint32_t *dur)
|
||||||
|
{
|
||||||
|
furi_mutex_acquire(s->mutex,FuriWaitForever);
|
||||||
|
idx = (s->idx + idx) % RAW_SAMPLES_NUM;
|
||||||
|
*level = s->level[idx];
|
||||||
|
*dur = s->dur[idx];
|
||||||
|
furi_mutex_release(s->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy one buffer to the other, including current index. */
|
||||||
|
void raw_samples_copy(RawSamplesBuffer *dst, RawSamplesBuffer *src) {
|
||||||
|
furi_mutex_acquire(src->mutex,FuriWaitForever);
|
||||||
|
furi_mutex_acquire(dst->mutex,FuriWaitForever);
|
||||||
|
dst->idx = src->idx;
|
||||||
|
dst->short_pulse_dur = src->short_pulse_dur;
|
||||||
|
memcpy(dst->level,src->level,sizeof(dst->level));
|
||||||
|
memcpy(dst->dur,src->dur,sizeof(dst->dur));
|
||||||
|
furi_mutex_release(src->mutex);
|
||||||
|
furi_mutex_release(dst->mutex);
|
||||||
|
}
|
||||||
29
applications/plugins/protoview/app_buffer.h
Normal file
29
applications/plugins/protoview/app_buffer.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||||
|
* See the LICENSE file for information about the license. */
|
||||||
|
|
||||||
|
/* Our circular buffer of raw samples, used in order to display
|
||||||
|
* the signal. */
|
||||||
|
|
||||||
|
#define RAW_SAMPLES_NUM 2048 /* Use a power of two: we take the modulo
|
||||||
|
of the index quite often to normalize inside
|
||||||
|
the range, and division is slow. */
|
||||||
|
|
||||||
|
typedef struct RawSamplesBuffer {
|
||||||
|
FuriMutex *mutex;
|
||||||
|
uint8_t level[RAW_SAMPLES_NUM];
|
||||||
|
uint32_t dur[RAW_SAMPLES_NUM];
|
||||||
|
uint32_t idx; /* Current idx (next to write). */
|
||||||
|
uint32_t total; /* Total samples: same as RAW_SAMPLES_NUM, we provide
|
||||||
|
this field for a cleaner interface with the user, but
|
||||||
|
we always use RAW_SAMPLES_NUM when taking the modulo so
|
||||||
|
the compiler can optimize % as bit masking. */
|
||||||
|
/* Signal features. */
|
||||||
|
uint32_t short_pulse_dur; /* Duration of the shortest pulse. */
|
||||||
|
} RawSamplesBuffer;
|
||||||
|
|
||||||
|
RawSamplesBuffer *raw_samples_alloc(void);
|
||||||
|
void raw_samples_reset(RawSamplesBuffer *s);
|
||||||
|
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);
|
||||||
|
void raw_samples_free(RawSamplesBuffer *s);
|
||||||
76
applications/plugins/protoview/app_subghz.c
Normal file
76
applications/plugins/protoview/app_subghz.c
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||||
|
* See the LICENSE file for information about the license. */
|
||||||
|
|
||||||
|
#include "app.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. */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Called after the application initialization in order to setup the
|
||||||
|
* subghz system and put it into idle state. If the user wants to start
|
||||||
|
* receiving we will call radio_rx() to start a receiving worker and
|
||||||
|
* associated thread. */
|
||||||
|
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);
|
||||||
|
furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow);
|
||||||
|
app->txrx->txrx_state = TxRxStateIDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Setup subghz to start receiving using a background worker. */
|
||||||
|
uint32_t radio_rx(ProtoViewApp* app) {
|
||||||
|
furi_assert(app);
|
||||||
|
if(!furi_hal_subghz_is_frequency_valid(app->frequency)) {
|
||||||
|
furi_crash(TAG" Incorrect RX frequency.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app->txrx->txrx_state == TxRxStateRx) return app->frequency;
|
||||||
|
|
||||||
|
furi_hal_subghz_idle(); /* Put it into idle state in case it is sleeping. */
|
||||||
|
uint32_t value = furi_hal_subghz_set_frequency_and_path(app->frequency);
|
||||||
|
FURI_LOG_E(TAG, "Switched to frequency: %lu", value);
|
||||||
|
furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow);
|
||||||
|
furi_hal_subghz_flush_rx();
|
||||||
|
furi_hal_subghz_rx();
|
||||||
|
|
||||||
|
furi_hal_subghz_start_async_rx(subghz_worker_rx_callback, app->txrx->worker);
|
||||||
|
subghz_worker_start(app->txrx->worker);
|
||||||
|
app->txrx->txrx_state = TxRxStateRx;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stop subghz worker (if active), put radio on idle state. */
|
||||||
|
void radio_rx_end(ProtoViewApp* app) {
|
||||||
|
furi_assert(app);
|
||||||
|
if (app->txrx->txrx_state == TxRxStateRx) {
|
||||||
|
if(subghz_worker_is_running(app->txrx->worker)) {
|
||||||
|
subghz_worker_stop(app->txrx->worker);
|
||||||
|
furi_hal_subghz_stop_async_rx();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
furi_hal_subghz_idle();
|
||||||
|
app->txrx->txrx_state = TxRxStateIDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Put radio on sleep. */
|
||||||
|
void radio_sleep(ProtoViewApp* app) {
|
||||||
|
furi_assert(app);
|
||||||
|
if (app->txrx->txrx_state == TxRxStateRx) {
|
||||||
|
/* We can't go from having an active RX worker to sleeping.
|
||||||
|
* Stop the RX subsystems first. */
|
||||||
|
radio_rx_end(app);
|
||||||
|
}
|
||||||
|
furi_hal_subghz_sleep();
|
||||||
|
app->txrx->txrx_state = TxRxStateSleep;
|
||||||
|
}
|
||||||
BIN
applications/plugins/protoview/appicon.png
Normal file
BIN
applications/plugins/protoview/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 B |
12
applications/plugins/protoview/application.fam
Normal file
12
applications/plugins/protoview/application.fam
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
App(
|
||||||
|
appid="protoview",
|
||||||
|
name="Protocols visualizer",
|
||||||
|
apptype=FlipperAppType.EXTERNAL,
|
||||||
|
entry_point="protoview_app_entry",
|
||||||
|
cdefines=["APP_PROTOVIEW"],
|
||||||
|
requires=["gui"],
|
||||||
|
stack_size=8 * 1024,
|
||||||
|
order=50,
|
||||||
|
fap_icon="appicon.png",
|
||||||
|
fap_category="Tools",
|
||||||
|
)
|
||||||
10
applications/plugins/protoview/binaries/README.md
Normal file
10
applications/plugins/protoview/binaries/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
This is the binary distribution of the application. If you don't want
|
||||||
|
to build it from source, just take `protoview.fap` file and drop it into the
|
||||||
|
following Flipper Zero location:
|
||||||
|
|
||||||
|
/ext/apps/Tools
|
||||||
|
|
||||||
|
The `ext` part means that we are in the SD card. So if you don't want
|
||||||
|
to use the Android (or other) application to upload the file,
|
||||||
|
you can just take out the SD card, insert it in your computer,
|
||||||
|
copy the fine into `apps/Tools`, and that's it.
|
||||||
BIN
applications/plugins/protoview/binaries/protoview.fap
Normal file
BIN
applications/plugins/protoview/binaries/protoview.fap
Normal file
Binary file not shown.
BIN
applications/plugins/protoview/images/ProtoViewSignal.jpg
Normal file
BIN
applications/plugins/protoview/images/ProtoViewSignal.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
120
applications/plugins/protoview/proto.c
Normal file
120
applications/plugins/protoview/proto.c
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||||
|
* See the LICENSE file for information about the license. */
|
||||||
|
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <lib/flipper_format/flipper_format_i.h>
|
||||||
|
#include <furi/core/string.h>
|
||||||
|
#include <lib/subghz/registry.h>
|
||||||
|
#include <lib/subghz/protocols/base.h>
|
||||||
|
#include "app_buffer.h"
|
||||||
|
|
||||||
|
#define TAG "PROTOVIEW-protocol"
|
||||||
|
|
||||||
|
const SubGhzProtocol subghz_protocol_protoview;
|
||||||
|
|
||||||
|
/* The feed() method puts data in the RawSamples global (protected by
|
||||||
|
* a mutex). */
|
||||||
|
extern RawSamplesBuffer *RawSamples;
|
||||||
|
|
||||||
|
/* This is totally dummy: we just define the decoder base for the async
|
||||||
|
* system to work but we don't really use it if not to collect raw
|
||||||
|
* data via the feed() method. */
|
||||||
|
typedef struct SubGhzProtocolDecoderprotoview {
|
||||||
|
SubGhzProtocolDecoderBase base;
|
||||||
|
} SubGhzProtocolDecoderprotoview;
|
||||||
|
|
||||||
|
void* subghz_protocol_decoder_protoview_alloc(SubGhzEnvironment* environment) {
|
||||||
|
UNUSED(environment);
|
||||||
|
|
||||||
|
SubGhzProtocolDecoderprotoview* instance =
|
||||||
|
malloc(sizeof(SubGhzProtocolDecoderprotoview));
|
||||||
|
instance->base.protocol = &subghz_protocol_protoview;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void subghz_protocol_decoder_protoview_free(void* context) {
|
||||||
|
furi_assert(context);
|
||||||
|
SubGhzProtocolDecoderprotoview* instance = context;
|
||||||
|
free(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
void subghz_protocol_decoder_protoview_reset(void* context) {
|
||||||
|
furi_assert(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* That's the only thig we really use of the protocol decoder
|
||||||
|
* implementation. We avoid the subghz provided abstractions and put
|
||||||
|
* the data in our simple abstraction: the RawSamples circular buffer. */
|
||||||
|
void subghz_protocol_decoder_protoview_feed(void* context, bool level, uint32_t duration) {
|
||||||
|
furi_assert(context);
|
||||||
|
UNUSED(context);
|
||||||
|
|
||||||
|
/* Add data to the circular buffer. */
|
||||||
|
raw_samples_add(RawSamples, level, duration);
|
||||||
|
// FURI_LOG_E(TAG, "FEED: %d %d", (int)level, (int)duration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The only scope of this method is to avoid duplicated messages in the
|
||||||
|
* Subghz history, which we don't use. */
|
||||||
|
uint8_t subghz_protocol_decoder_protoview_get_hash_data(void* context) {
|
||||||
|
furi_assert(context);
|
||||||
|
return 123;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Not used. */
|
||||||
|
bool subghz_protocol_decoder_protoview_serialize(
|
||||||
|
void* context,
|
||||||
|
FlipperFormat* flipper_format,
|
||||||
|
SubGhzRadioPreset* preset)
|
||||||
|
{
|
||||||
|
UNUSED(context);
|
||||||
|
UNUSED(flipper_format);
|
||||||
|
UNUSED(preset);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Not used. */
|
||||||
|
bool subghz_protocol_decoder_protoview_deserialize(void* context, FlipperFormat* flipper_format)
|
||||||
|
{
|
||||||
|
UNUSED(context);
|
||||||
|
UNUSED(flipper_format);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void subhz_protocol_decoder_protoview_get_string(void* context, FuriString* output)
|
||||||
|
{
|
||||||
|
furi_assert(context);
|
||||||
|
furi_string_cat_printf(output, "Protoview");
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubGhzProtocolDecoder subghz_protocol_protoview_decoder = {
|
||||||
|
.alloc = subghz_protocol_decoder_protoview_alloc,
|
||||||
|
.free = subghz_protocol_decoder_protoview_free,
|
||||||
|
.reset = subghz_protocol_decoder_protoview_reset,
|
||||||
|
.feed = subghz_protocol_decoder_protoview_feed,
|
||||||
|
.get_hash_data = subghz_protocol_decoder_protoview_get_hash_data,
|
||||||
|
.serialize = subghz_protocol_decoder_protoview_serialize,
|
||||||
|
.deserialize = subghz_protocol_decoder_protoview_deserialize,
|
||||||
|
.get_string = subhz_protocol_decoder_protoview_get_string,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Well, we don't really target a specific protocol. So let's put flags
|
||||||
|
* that make sense. */
|
||||||
|
const SubGhzProtocol subghz_protocol_protoview = {
|
||||||
|
.name = "Protoview",
|
||||||
|
.type = SubGhzProtocolTypeStatic,
|
||||||
|
.flag = SubGhzProtocolFlag_AM | SubGhzProtocolFlag_FM | SubGhzProtocolFlag_Decodable,
|
||||||
|
.decoder = &subghz_protocol_protoview_decoder,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Our table has just the single dummy protocol we defined for the
|
||||||
|
* sake of data collection. */
|
||||||
|
const SubGhzProtocol* protoview_protocol_registry_items[] = {
|
||||||
|
&subghz_protocol_protoview,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SubGhzProtocolRegistry protoview_protocol_registry = {
|
||||||
|
.items = protoview_protocol_registry_items,
|
||||||
|
.size = COUNT_OF(protoview_protocol_registry_items)
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user