mirror of
https://github.com/DarkFlippers/unleashed-firmware.git
synced 2025-12-12 20:49:49 +04:00
@@ -1,17 +1,15 @@
|
||||
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):
|
||||
ProtoView is a digital signal detection, visualization, editing and reply tool for the [Flipper Zero](https://flipperzero.one/). The Flipper default application, called Subghz, is able to identify certain RF protocols, but when the exact protocol is not implemented (and there are many undocumented and unimplemented ones, such as the ones in use in TPMS systems, car keys and many others), 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 TPMS signal produced by a Renault tire):
|
||||
|
||||

|
||||
|
||||
This is often enough to make an initial idea about the encoding used
|
||||
and if the selected modulation is correct.
|
||||
and if the selected modulation is correct. For example, in the signal above
|
||||
you can see a set of regular pulses and gaps used for synchronization, and then
|
||||
a sequence of bits encoded in [Manchester](https://en.wikipedia.org/wiki/Manchester_code) line code. If you study these things for five minutes, you'll find yourself able to decode the bits with naked eyes.
|
||||
|
||||
Other than that, ProtoView is able to decode a few interesting protocols:
|
||||
## Decoding capabilities
|
||||
|
||||
Other than showing the raw signal, ProtoView is able to decode a few interesting protocols:
|
||||
|
||||
* TPMS sensors: Renault, Toyota, Schrader, Citroen, Ford.
|
||||
* Microchip HSC200/300/301 Keeloq protocol.
|
||||
@@ -23,25 +21,39 @@ Other than that, ProtoView is able to decode a few interesting protocols:
|
||||
|
||||
The app implements a framework that makes adding and experimenting with new
|
||||
protocols very simple. Check the `protocols` directory to see how the
|
||||
API works.
|
||||
API works, or read the full documentation at the end of this `README` file.
|
||||
The gist of it is that the decoder receives the signal already converted into
|
||||
a bitmap, where each bit represents a short pulse duration. Then there are
|
||||
functions to seek specific sync/preamble sequences inside the bitmap, to decode
|
||||
from different line codes, to compute checksums and so forth.
|
||||
|
||||
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).
|
||||
## Signals transmission capabilities
|
||||
|
||||
Once ProtoView decodes a given message, it is able to *resample* it
|
||||
in pulses and gaps of the theoretical duration, and send the signal again
|
||||
via the Flipper TX capabilities. The captured signal can be sent
|
||||
to different frequencies and modulations than the ones it was captured
|
||||
from.
|
||||
|
||||
For selected protocols, that implement the message creation methods,
|
||||
ProtoView is also able to edit the message received, modify fields,
|
||||
and finally re-detect the new produced signal and resend it. Signals
|
||||
can also be produced from scratch, by setting all the fields to appropriate
|
||||
values.
|
||||
|
||||
## A well-documented app for the Flipper
|
||||
|
||||
The secondary goal of ProtoView is to provide a well-documented application for the Flipper (even if ProtoView is a pretty atypical application: it doesn't make use of the standard widgets and other abstractions provided by the framework).
|
||||
Most apps dealing with the *subghz subsystem* of the Flipper (the abstraction used to work with the [CC1101 chip](https://www.ti.com/product/CC1101)) tend to be complicated and completely undocumented.
|
||||
Unfortunately, this is also true for the firmware of the device.
|
||||
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.
|
||||
lacks comments and documentation (and sometimes makes use of abstractions more convoluted than needed), so it is possible to make some ideas of how things work just grepping inside. In order to develop this application, I ended reading most parts of the firmware of the device.
|
||||
|
||||
# Detection algorithm
|
||||
## 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
|
||||
In order to detect and 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.
|
||||
|
||||
@@ -49,25 +61,34 @@ 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
|
||||
a maximum of three different duration classes, plus or minus a given percentage.
|
||||
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
|
||||
However, often, pulses of the same theoretical
|
||||
length have slightly different lengths in the case of high and low level
|
||||
(RF on or off), so we classify them separately for robustness.
|
||||
(RF on or off), so the detector classifies them separately for robustness.
|
||||
|
||||
Once the raw signal is detected, the registered protocol decoders are called
|
||||
against it, in the hope some of the decoders will make sense of the signal.
|
||||
|
||||
# Usage
|
||||
|
||||
The application shows the longest coherent signal detected so far.
|
||||
In the main screen, the application shows the longest coherent signal detected so far. The user can switch to other views pressing the LEFT and RIGHT keys. The BACK key will return back to the main screen. Long pressing BACK will quit the application.
|
||||
|
||||
* 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.
|
||||
## Main raw signal screen
|
||||
|
||||
* A long press of the OK button resets the current signal, in order to capture a new one.
|
||||
* The UP and DOWN buttons change the scale. Default is 100us per pixel, but it will be adapted to the signal just captured.
|
||||
* A long press of the LEFT and RIGHT keys will pan the signal, to see what was transmitted before/after the current shown range.
|
||||
* A short press to OK will recenter the signal and set the scale back to the default for the specific pulse duration detected.
|
||||
|
||||
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.
|
||||
to that point, and starting from there it could be just noise. However the
|
||||
signal decoders will not get just up to this point, but will get more:
|
||||
sometimes the low level detector can't make sense of a signal that the
|
||||
protocol-specific decoder can understand fully.
|
||||
|
||||
If the protocol is decoded, the bottom-left corner of the screen
|
||||
will show the name of the protocol, and going in the next screen
|
||||
@@ -81,12 +102,47 @@ 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:
|
||||
## Info screen
|
||||
|
||||
If a signal was detected, the info view will show the details about the signal. If the signal has more data that a single screen can fit, pressing OK will show the next fields. Pressing DOWN will go to a sub-view with an oscilloscope-alike representation of the signal, from there you can:
|
||||
|
||||
1. Resend the signal, by pressing OK.
|
||||
2. Save the signal as `.sub` file, by long pressing OK.
|
||||
|
||||
When resending, you can select a different frequency and modulation if you
|
||||
wish.
|
||||
|
||||
## Frequency and modulation screen
|
||||
|
||||
In this view you can just set the frequency and modulation you want to use.
|
||||
There are special modulations for TPMS signals: they need an higher data
|
||||
rate.
|
||||
|
||||
* 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 433.92 OOK650 and 868.35 Mhz in OOK or 2FSK.
|
||||
* For TPMS try 433.92 in TPMS modulation (FSK optimized for these signals).
|
||||
* For TPMS try 433.92 in TPMS1 or TPMS2 modulations (FSK and OOK custom modulations optimized for these signals, that have a relatively high data rate).
|
||||
|
||||
## Signal creator
|
||||
|
||||
In this view, you can do two things:
|
||||
|
||||
1. Select one of the protocols supporting message creation, and create a signal from scratch.
|
||||
2. If there is already a detected signal, you can modify the signal.
|
||||
|
||||
This is how it works:
|
||||
|
||||
1. Select one of the protocols (the one of the currently detected signal will be already provided as default, if any, and if it supports message creation).
|
||||
2. Fill the fields. Use LEFT and RIGHT to change the values of integers, or just press OK and enter the new value with the Fliper keyboard widget.
|
||||
3. When you are done, long press OK to build the message. Then press BACK in order to see it.
|
||||
4. Go to the INFO view, and then DOWN to the signal sending/saving subview in order to send or save it.
|
||||
|
||||
## Direct sampling screen
|
||||
|
||||
This final screen shows in real time the high and low level that the Flipper
|
||||
RF chip, the CC1101, is receiving. This will makes very easy to understand
|
||||
if a given frequency is targeted by something other than noise. This mode is
|
||||
fun to watch, resembling an old CRT TV set.
|
||||
|
||||
# Installing the app from source
|
||||
|
||||
@@ -113,6 +169,107 @@ 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.
|
||||
|
||||
# Protocols decoders API
|
||||
|
||||
Writing a protocol decoder is not hard, and requires to write three
|
||||
different methods.
|
||||
|
||||
1. `decode()`. This is mandatory, and is used in order to turn a known signal into a set of fields containing certain informations. For instance for a thermometer sending data via RF, a raw message will be decoded into fields like temperature, humidity, station ID and so forth.
|
||||
2. `get_fields()`. Optional, only needed if the protocol supports creating and editing signals. This method just returns the fields names, types and defaults. The app will use this list in order to allow the user to set values. The populated fields will be passed to the `build_message()` method later.
|
||||
3. `build_message()`. This method gets a set of fields representing the parameters of the protocol, as set by the user, and will create a low level signal composed of pulses and gaps of specific durations.
|
||||
|
||||
## `decode()` method
|
||||
|
||||
bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info);
|
||||
|
||||
The method gets a bitmap `bits` long `numbytes` bytes but actually containing `bumbits` valid bits. Each bit represents a pulse of gap of the duration of the shortest time detected in the protocol (this is often called *te*, in the RF protocols jargon). So, for instance, if a signal is composed of pulses and gaps of around 500 and 1000 microseconds, each bit in the bitmap will represent 500 microseconds.
|
||||
|
||||
Continuing with the example above, if the received signal was composed of a 1000 microseconds gap, then a 500 microsecond pulse, then a 500 microsecond gap and finally a 1000 microseconds pulse, its bitmap representation will be:
|
||||
|
||||
001011
|
||||
|
||||
To access the bitmap, the decoder can use the following API:
|
||||
|
||||
bool bitmap_get(uint8_t *b, uint32_t blen, uint32_t bitpos);
|
||||
|
||||
The `blen` parameter will be set to what the decode method gets
|
||||
as `numbytes`, and is used to prevent overflows. This way if `bitpos`
|
||||
is out of range, nothing bad happens.
|
||||
|
||||
There are function to match and seek specific patterns inside the signal:
|
||||
|
||||
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);
|
||||
|
||||
Finally, there are functions to convert from different line codes:
|
||||
|
||||
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);
|
||||
uint32_t convert_from_diff_manchester(uint8_t *buf, uint64_t buflen, uint8_t *bits, uint32_t len, uint32_t off, bool previous);
|
||||
|
||||
This method can also access the short pulse duration by inspecting the
|
||||
`info->short_pulse_dur` field (in microseconds).
|
||||
|
||||
Please check the `b4b1.c` file for an easy to understand example of the decoder implementation.
|
||||
|
||||
If the decoder actually detected a message, it will return `true` and will return a set of fields, like thata:
|
||||
|
||||
fieldset_add_bytes(info->fieldset,"id",d,5);
|
||||
fieldset_add_uint(info->fieldset,"button",d[2]&0xf,4);
|
||||
|
||||
## `get_fields()` method.
|
||||
|
||||
static void get_fields(ProtoViewFieldSet *fieldset);
|
||||
|
||||
This method will be basically a copy of the final part of `decode()`, as
|
||||
it also needs to return the set of fields this protocol is composed of.
|
||||
However instead of returning the values of an actual decoded message, it
|
||||
will just provide their default values for the signal creator view.
|
||||
|
||||
Note that the `build_message()` method is guaranteed to receive the
|
||||
same exact fields in the same exact order.
|
||||
|
||||
## `build_message()` method.
|
||||
|
||||
void build_message(RawSamplesBuffer *samples, ProtoViewFieldSet *fs);
|
||||
|
||||
This method is responsible of creating a signal from scratch, by
|
||||
appending gaps and pulses of the specific duration into `samples`
|
||||
using the following API:
|
||||
|
||||
raw_samples_add(RawSamplesBuffer *samples, bool level, uint32_t duration);
|
||||
|
||||
Level can be true (pules) or false (gap). Duration is in microseconds.
|
||||
The method receives a set of fields in `fs`. Each field is accessible
|
||||
directly accessing `fs->fields[... field index ...]`, where the field
|
||||
index is 0, 1, 2, ... in the same order as `get_fields()` returned the
|
||||
fields.
|
||||
|
||||
For now, you can access the fields in the raw way, by getting the
|
||||
values directly from the data structure representing each field:
|
||||
|
||||
```
|
||||
typedef struct {
|
||||
ProtoViewFieldType type;
|
||||
uint32_t len; // Depends on type:
|
||||
// Bits for integers (signed,unsigned,binary,hex).
|
||||
// Number of characters for strings.
|
||||
// Number of nibbles for bytes (1 for each 4 bits).
|
||||
// Number of digits after dot for floats.
|
||||
char *name; // Field name.
|
||||
union {
|
||||
char *str; // String type.
|
||||
int64_t value; // Signed integer type.
|
||||
uint64_t uvalue; // Unsigned integer type.
|
||||
uint8_t *bytes; // Raw bytes type.
|
||||
float fvalue; // Float type.
|
||||
};
|
||||
} ProtoViewField;
|
||||
|
||||
```
|
||||
|
||||
However later the app will likely provide a set of macros to do it
|
||||
in a more future-proof way.
|
||||
|
||||
# License
|
||||
|
||||
The code is released under the BSD license.
|
||||
@@ -128,3 +285,5 @@ A big thank you to the RTL433 author, [Benjamin Larsson](https://github.com/merb
|
||||
* As a sourve of documentation for protocols.
|
||||
* As an awesome way to visualize and understand protocols, via [these great web tools](https://triq.org/).
|
||||
* To have tons of fun with RTLSDR in general, now and in the past.
|
||||
|
||||
The application icon was designed by Stefano Liuzzo.
|
||||
|
||||
@@ -3,32 +3,6 @@
|
||||
|
||||
#include "app.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;
|
||||
|
||||
@@ -42,6 +16,7 @@ extern const SubGhzProtocolRegistry protoview_protocol_registry;
|
||||
* and setting color to black. */
|
||||
static void render_callback(Canvas* const canvas, void* ctx) {
|
||||
ProtoViewApp* app = ctx;
|
||||
furi_mutex_acquire(app->view_updating_mutex, FuriWaitForever);
|
||||
|
||||
/* Clear screen. */
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
@@ -64,6 +39,9 @@ static void render_callback(Canvas* const canvas, void* ctx) {
|
||||
case ViewDirectSampling:
|
||||
render_view_direct_sampling(canvas, app);
|
||||
break;
|
||||
case ViewBuildMessage:
|
||||
render_view_build_message(canvas, app);
|
||||
break;
|
||||
default:
|
||||
furi_crash(TAG "Invalid view selected");
|
||||
break;
|
||||
@@ -71,6 +49,7 @@ static void render_callback(Canvas* const canvas, void* ctx) {
|
||||
|
||||
/* Draw the alert box if set. */
|
||||
ui_draw_alert_if_needed(canvas, app);
|
||||
furi_mutex_release(app->view_updating_mutex);
|
||||
}
|
||||
|
||||
/* Here all we do is putting the events into the queue that will be handled
|
||||
@@ -88,6 +67,9 @@ static void input_callback(InputEvent* input_event, void* ctx) {
|
||||
* special views ViewGoNext and ViewGoPrev in order to move to
|
||||
* the logical next/prev view. */
|
||||
static void app_switch_view(ProtoViewApp* app, ProtoViewCurrentView switchto) {
|
||||
furi_mutex_acquire(app->view_updating_mutex, FuriWaitForever);
|
||||
|
||||
/* Switch to the specified view. */
|
||||
ProtoViewCurrentView old = app->current_view;
|
||||
if(switchto == ViewGoNext) {
|
||||
app->current_view++;
|
||||
@@ -102,9 +84,10 @@ static void app_switch_view(ProtoViewApp* app, ProtoViewCurrentView switchto) {
|
||||
}
|
||||
ProtoViewCurrentView new = app->current_view;
|
||||
|
||||
/* Call the enter/exit view callbacks if needed. */
|
||||
/* Call the exit view callbacks. */
|
||||
if(old == ViewDirectSampling) view_exit_direct_sampling(app);
|
||||
if(new == ViewDirectSampling) view_enter_direct_sampling(app);
|
||||
if(old == ViewBuildMessage) view_exit_build_message(app);
|
||||
if(old == ViewInfo) view_exit_info(app);
|
||||
/* The frequency/modulation settings are actually a single view:
|
||||
* as long as the user stays between the two modes of this view we
|
||||
* don't need to call the exit-view callback. */
|
||||
@@ -112,12 +95,24 @@ static void app_switch_view(ProtoViewApp* app, ProtoViewCurrentView switchto) {
|
||||
(old == ViewModulationSettings && new != ViewFrequencySettings))
|
||||
view_exit_settings(app);
|
||||
|
||||
/* Set the current subview of the view we just left to zero, that is
|
||||
* the main subview of the view. When re re-enter it we want to see
|
||||
* the main thing. */
|
||||
app->current_subview[old] = 0;
|
||||
/* Reset the view private data each time, before calling the enter
|
||||
* callbacks that may want to setup some state. */
|
||||
memset(app->view_privdata, 0, PROTOVIEW_VIEW_PRIVDATA_LEN);
|
||||
|
||||
/* Call the enter view callbacks after all the exit callback
|
||||
* of the old view was already executed. */
|
||||
if(new == ViewDirectSampling) view_enter_direct_sampling(app);
|
||||
if(new == ViewBuildMessage) view_enter_build_message(app);
|
||||
|
||||
/* Set the current subview of the view we just left to zero. This is
|
||||
* the main subview of the old view. When we re-enter the view we are
|
||||
* lefting, we want to see the main thing again. */
|
||||
app->current_subview[old] = 0;
|
||||
|
||||
/* If there is an alert on screen, dismiss it: if the user is
|
||||
* switching view she already read it. */
|
||||
ui_dismiss_alert(app);
|
||||
furi_mutex_release(app->view_updating_mutex);
|
||||
}
|
||||
|
||||
/* Allocate the application state and initialize a number of stuff.
|
||||
@@ -146,6 +141,7 @@ ProtoViewApp* protoview_app_alloc() {
|
||||
app->show_text_input = false;
|
||||
app->alert_dismiss_time = 0;
|
||||
app->current_view = ViewRawPulses;
|
||||
app->view_updating_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
for(int j = 0; j < ViewLast; j++) app->current_subview[j] = 0;
|
||||
app->direct_sampling_enabled = false;
|
||||
app->view_privdata = malloc(PROTOVIEW_VIEW_PRIVDATA_LEN);
|
||||
@@ -162,25 +158,11 @@ ProtoViewApp* protoview_app_alloc() {
|
||||
// Init Worker & Protocol
|
||||
app->txrx = malloc(sizeof(ProtoViewTxRx));
|
||||
|
||||
/* Setup rx worker and environment. */
|
||||
/* Setup rx state. */
|
||||
app->txrx->freq_mod_changed = false;
|
||||
app->txrx->debug_timer_sampling = false;
|
||||
app->txrx->last_g0_change_time = DWT->CYCCNT;
|
||||
app->txrx->last_g0_value = false;
|
||||
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);
|
||||
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]. */
|
||||
@@ -207,17 +189,13 @@ void protoview_app_free(ProtoViewApp* app) {
|
||||
furi_record_close(RECORD_GUI);
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
furi_message_queue_free(app->event_queue);
|
||||
furi_mutex_free(app->view_updating_mutex);
|
||||
app->gui = NULL;
|
||||
|
||||
// Frequency setting.
|
||||
subghz_setting_free(app->setting);
|
||||
|
||||
// Worker stuff.
|
||||
if(!app->txrx->debug_timer_sampling) {
|
||||
subghz_receiver_free(app->txrx->receiver);
|
||||
subghz_environment_free(app->txrx->environment);
|
||||
subghz_worker_free(app->txrx->worker);
|
||||
}
|
||||
free(app->txrx);
|
||||
|
||||
// Raw samples buffers.
|
||||
@@ -247,9 +225,25 @@ static void timer_callback(void* ctx) {
|
||||
}
|
||||
if(delta < RawSamples->total / 2) return;
|
||||
app->signal_last_scan_idx = RawSamples->idx;
|
||||
scan_for_signal(app);
|
||||
scan_for_signal(app, RawSamples, ProtoViewModulations[app->modulation].duration_filter);
|
||||
}
|
||||
|
||||
/* This is the navigation callback we use in the view dispatcher used
|
||||
* to display the "text input" widget, that is the keyboard to get text.
|
||||
* The text input view is implemented to ignore the "back" short press,
|
||||
* so the event is not consumed and is handled by the view dispatcher.
|
||||
* However the view dispatcher implementation has the strange behavior that
|
||||
* if no navigation callback is set, it will not stop when handling back.
|
||||
*
|
||||
* We just need a dummy callback returning false. We believe the
|
||||
* implementation should be changed and if no callback is set, it should be
|
||||
* the same as returning false. */
|
||||
static bool keyboard_view_dispatcher_navigation_callback(void* ctx) {
|
||||
UNUSED(ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* App entry point, as specified in application.fam. */
|
||||
int32_t protoview_app_entry(void* p) {
|
||||
UNUSED(p);
|
||||
ProtoViewApp* app = protoview_app_alloc();
|
||||
@@ -314,6 +308,9 @@ int32_t protoview_app_entry(void* p) {
|
||||
case ViewDirectSampling:
|
||||
process_input_direct_sampling(app, input);
|
||||
break;
|
||||
case ViewBuildMessage:
|
||||
process_input_build_message(app, input);
|
||||
break;
|
||||
default:
|
||||
furi_crash(TAG "Invalid view selected");
|
||||
break;
|
||||
@@ -337,6 +334,11 @@ int32_t protoview_app_entry(void* p) {
|
||||
* and activate it. */
|
||||
app->view_dispatcher = view_dispatcher_alloc();
|
||||
view_dispatcher_enable_queue(app->view_dispatcher);
|
||||
/* We need to set a navigation callback for the view dispatcher
|
||||
* otherwise when the user presses back on the keyboard to
|
||||
* abort, the dispatcher will not stop. */
|
||||
view_dispatcher_set_navigation_event_callback(
|
||||
app->view_dispatcher, keyboard_view_dispatcher_navigation_callback);
|
||||
app->text_input = text_input_alloc();
|
||||
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
||||
view_dispatcher_add_view(
|
||||
|
||||
@@ -17,11 +17,8 @@
|
||||
#include <gui/modules/text_input.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>
|
||||
#include "app_buffer.h"
|
||||
#include "raw_samples.h"
|
||||
|
||||
#define TAG "ProtoView"
|
||||
#define PROTOVIEW_RAW_VIEW_DEFAULT_SCALE 100 // 100us is 1 pixel by default
|
||||
@@ -30,7 +27,14 @@
|
||||
|
||||
#define DEBUG_MSG 0
|
||||
|
||||
/* Forward declarations. */
|
||||
|
||||
typedef struct ProtoViewApp ProtoViewApp;
|
||||
typedef struct ProtoViewMsgInfo ProtoViewMsgInfo;
|
||||
typedef struct ProtoViewFieldSet ProtoViewFieldSet;
|
||||
typedef struct ProtoViewDecoder ProtoViewDecoder;
|
||||
|
||||
/* ============================== enumerations ============================== */
|
||||
|
||||
/* Subghz system state */
|
||||
typedef enum {
|
||||
@@ -46,6 +50,7 @@ typedef enum {
|
||||
ViewInfo,
|
||||
ViewFrequencySettings,
|
||||
ViewModulationSettings,
|
||||
ViewBuildMessage,
|
||||
ViewDirectSampling,
|
||||
ViewLast, /* Just a sentinel to wrap around. */
|
||||
|
||||
@@ -55,12 +60,17 @@ typedef enum {
|
||||
ViewGoPrev,
|
||||
} ProtoViewCurrentView;
|
||||
|
||||
/* ================================== RX/TX ================================= */
|
||||
|
||||
typedef struct {
|
||||
const char* name; // Name to show to the user.
|
||||
const char* id; // Identifier in the Flipper API/file.
|
||||
FuriHalSubGhzPreset preset; // The preset ID.
|
||||
uint8_t* custom; // If not null, a set of registers for
|
||||
// the CC1101, specifying a custom preset.
|
||||
uint8_t* custom; /* If not null, a set of registers for
|
||||
the CC1101, specifying a custom preset.*/
|
||||
uint32_t duration_filter; /* Ignore pulses and gaps that are less
|
||||
than the specified microseconds. This
|
||||
depends on the data rate. */
|
||||
} ProtoViewModulation;
|
||||
|
||||
extern ProtoViewModulation ProtoViewModulations[]; /* In app_subghz.c */
|
||||
@@ -72,9 +82,6 @@ struct ProtoViewTxRx {
|
||||
bool freq_mod_changed; /* The user changed frequency and/or modulation
|
||||
from the interface. There is to restart the
|
||||
radio with the right parameters. */
|
||||
SubGhzWorker* worker; /* Our background worker. */
|
||||
SubGhzEnvironment* environment;
|
||||
SubGhzReceiver* receiver;
|
||||
TxRxState txrx_state; /* Receiving, idle or sleeping? */
|
||||
|
||||
/* Timer sampling mode state. */
|
||||
@@ -88,34 +95,8 @@ 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. */
|
||||
char info4[PROTOVIEW_MSG_STR_LEN]; /* Protocol specific info line 4. */
|
||||
/* Low level information of the detected signal: the following are filled
|
||||
* by the protocol decoding function: */
|
||||
uint32_t start_off; /* Pulses start offset in the bitmap. */
|
||||
uint32_t pulses_count; /* Number of pulses of the full message. */
|
||||
/* The following are passed already filled to the decoder. */
|
||||
uint32_t short_pulse_dur; /* Microseconds duration of the short pulse. */
|
||||
/* The following are filled by ProtoView core after the decoder returned
|
||||
* success. */
|
||||
uint8_t* bits; /* Bitmap with the signal. */
|
||||
uint32_t bits_bytes; /* Number of full bytes in the bitmap, that
|
||||
is 'pulses_count/8' rounded to the next
|
||||
integer. */
|
||||
} ProtoViewMsgInfo;
|
||||
/* ============================== Main app state ============================ */
|
||||
|
||||
/* Our main application context. */
|
||||
#define ALERT_MAX_LEN 32
|
||||
struct ProtoViewApp {
|
||||
/* GUI */
|
||||
@@ -124,6 +105,10 @@ struct ProtoViewApp {
|
||||
ViewPort* view_port; /* We just use a raw viewport and we render
|
||||
everything into the low level canvas. */
|
||||
ProtoViewCurrentView current_view; /* Active left-right view ID. */
|
||||
FuriMutex* view_updating_mutex; /* The Flipper GUI calls the screen redraw
|
||||
callback in a different thread. We
|
||||
use this mutex to protect the redraw
|
||||
from changes in app->view_privdata. */
|
||||
int current_subview[ViewLast]; /* Active up-down subview ID. */
|
||||
FuriMessageQueue* event_queue; /* Keypress events go here. */
|
||||
|
||||
@@ -174,6 +159,64 @@ struct ProtoViewApp {
|
||||
ProtoViewModulations table. */
|
||||
};
|
||||
|
||||
/* =========================== Protocols decoders =========================== */
|
||||
|
||||
/* 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 {
|
||||
ProtoViewDecoder* decoder; /* The decoder that decoded the message. */
|
||||
ProtoViewFieldSet* fieldset; /* Decoded fields. */
|
||||
/* Low level information of the detected signal: the following are filled
|
||||
* by the protocol decoding function: */
|
||||
uint32_t start_off; /* Pulses start offset in the bitmap. */
|
||||
uint32_t pulses_count; /* Number of pulses of the full message. */
|
||||
/* The following are passed already filled to the decoder. */
|
||||
uint32_t short_pulse_dur; /* Microseconds duration of the short pulse. */
|
||||
/* The following are filled by ProtoView core after the decoder returned
|
||||
* success. */
|
||||
uint8_t* bits; /* Bitmap with the signal. */
|
||||
uint32_t bits_bytes; /* Number of full bytes in the bitmap, that
|
||||
is 'pulses_count/8' rounded to the next
|
||||
integer. */
|
||||
} ProtoViewMsgInfo;
|
||||
|
||||
/* This structures describe a set of protocol fields. It is used by decoders
|
||||
* supporting message building to receive and return information about the
|
||||
* protocol. */
|
||||
typedef enum {
|
||||
FieldTypeStr,
|
||||
FieldTypeSignedInt,
|
||||
FieldTypeUnsignedInt,
|
||||
FieldTypeBinary,
|
||||
FieldTypeHex,
|
||||
FieldTypeBytes,
|
||||
FieldTypeFloat,
|
||||
} ProtoViewFieldType;
|
||||
|
||||
typedef struct {
|
||||
ProtoViewFieldType type;
|
||||
uint32_t len; // Depends on type:
|
||||
// Bits for integers (signed,unsigned,binary,hex).
|
||||
// Number of characters for strings.
|
||||
// Number of nibbles for bytes (1 for each 4 bits).
|
||||
// Number of digits after dot for floats.
|
||||
char* name; // Field name.
|
||||
union {
|
||||
char* str; // String type.
|
||||
int64_t value; // Signed integer type.
|
||||
uint64_t uvalue; // Unsigned integer type.
|
||||
uint8_t* bytes; // Raw bytes type.
|
||||
float fvalue; // Float type.
|
||||
};
|
||||
} ProtoViewField;
|
||||
|
||||
typedef struct ProtoViewFieldSet {
|
||||
ProtoViewField** fields;
|
||||
uint32_t numfields;
|
||||
} ProtoViewFieldSet;
|
||||
|
||||
typedef struct ProtoViewDecoder {
|
||||
const char* name; /* Protocol name. */
|
||||
/* The decode function takes a buffer that is actually a bitmap, with
|
||||
@@ -184,11 +227,19 @@ typedef struct ProtoViewDecoder {
|
||||
* 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);
|
||||
/* This method is used by the decoder to return the fields it needs
|
||||
* in order to build a new message. This way the message builder view
|
||||
* can ask the user to fill the right set of fields of the specified
|
||||
* type. */
|
||||
void (*get_fields)(ProtoViewFieldSet* fields);
|
||||
/* This method takes the fields supported by the decoder, and
|
||||
* renders a message in 'samples'. */
|
||||
void (*build_message)(RawSamplesBuffer* samples, ProtoViewFieldSet* fields);
|
||||
} ProtoViewDecoder;
|
||||
|
||||
extern RawSamplesBuffer *RawSamples, *DetectedSamples;
|
||||
|
||||
/* app_radio.c */
|
||||
/* app_subghz.c */
|
||||
void radio_begin(ProtoViewApp* app);
|
||||
uint32_t radio_rx(ProtoViewApp* app);
|
||||
void radio_idle(ProtoViewApp* app);
|
||||
@@ -197,11 +248,12 @@ void radio_sleep(ProtoViewApp* app);
|
||||
void raw_sampling_worker_start(ProtoViewApp* app);
|
||||
void raw_sampling_worker_stop(ProtoViewApp* app);
|
||||
void radio_tx_signal(ProtoViewApp* app, FuriHalSubGhzAsyncTxCallback data_feeder, void* ctx);
|
||||
void protoview_rx_callback(bool level, uint32_t duration, void* context);
|
||||
|
||||
/* signal.c */
|
||||
uint32_t duration_delta(uint32_t a, uint32_t b);
|
||||
void reset_current_signal(ProtoViewApp* app);
|
||||
void scan_for_signal(ProtoViewApp* app);
|
||||
void scan_for_signal(ProtoViewApp* app, RawSamplesBuffer* source, uint32_t min_duration);
|
||||
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_copy(
|
||||
@@ -213,7 +265,7 @@ void bitmap_copy(
|
||||
uint32_t soff,
|
||||
uint32_t count);
|
||||
void bitmap_set_pattern(uint8_t* b, uint32_t blen, uint32_t off, const char* pat);
|
||||
void bitmap_reverse_bytes(uint8_t* p, uint32_t len);
|
||||
void bitmap_reverse_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,
|
||||
@@ -221,6 +273,15 @@ uint32_t bitmap_seek_bits(
|
||||
uint32_t startpos,
|
||||
uint32_t maxbits,
|
||||
const char* bits);
|
||||
bool bitmap_match_bitmap(
|
||||
uint8_t* b1,
|
||||
uint32_t b1len,
|
||||
uint32_t b1off,
|
||||
uint8_t* b2,
|
||||
uint32_t b2len,
|
||||
uint32_t b2off,
|
||||
uint32_t cmplen);
|
||||
void bitmap_to_string(char* dst, uint8_t* b, uint32_t blen, uint32_t off, uint32_t len);
|
||||
uint32_t convert_from_line_code(
|
||||
uint8_t* buf,
|
||||
uint64_t buflen,
|
||||
@@ -251,9 +312,15 @@ 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 render_view_build_message(Canvas* const canvas, ProtoViewApp* app);
|
||||
void process_input_build_message(ProtoViewApp* app, InputEvent input);
|
||||
void view_enter_build_message(ProtoViewApp* app);
|
||||
void view_exit_build_message(ProtoViewApp* app);
|
||||
void view_enter_direct_sampling(ProtoViewApp* app);
|
||||
void view_exit_direct_sampling(ProtoViewApp* app);
|
||||
void view_exit_settings(ProtoViewApp* app);
|
||||
void view_exit_info(ProtoViewApp* app);
|
||||
void adjust_raw_view_scale(ProtoViewApp* app, uint32_t short_pulse_dur);
|
||||
|
||||
/* ui.c */
|
||||
int ui_get_current_subview(ProtoViewApp* app);
|
||||
@@ -276,5 +343,32 @@ void canvas_draw_str_with_border(
|
||||
Color text_color,
|
||||
Color border_color);
|
||||
|
||||
/* fields.c */
|
||||
void fieldset_free(ProtoViewFieldSet* fs);
|
||||
ProtoViewFieldSet* fieldset_new(void);
|
||||
void fieldset_add_int(ProtoViewFieldSet* fs, const char* name, int64_t val, uint8_t bits);
|
||||
void fieldset_add_uint(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits);
|
||||
void fieldset_add_hex(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits);
|
||||
void fieldset_add_bin(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits);
|
||||
void fieldset_add_str(ProtoViewFieldSet* fs, const char* name, const char* s, size_t len);
|
||||
void fieldset_add_bytes(
|
||||
ProtoViewFieldSet* fs,
|
||||
const char* name,
|
||||
const uint8_t* bytes,
|
||||
uint32_t count);
|
||||
void fieldset_add_float(
|
||||
ProtoViewFieldSet* fs,
|
||||
const char* name,
|
||||
float val,
|
||||
uint32_t digits_after_dot);
|
||||
const char* field_get_type_name(ProtoViewField* f);
|
||||
int field_to_string(char* buf, size_t len, ProtoViewField* f);
|
||||
bool field_set_from_string(ProtoViewField* f, char* buf, size_t len);
|
||||
bool field_incr_value(ProtoViewField* f, int incr);
|
||||
void fieldset_copy_matching_fields(ProtoViewFieldSet* dst, ProtoViewFieldSet* src);
|
||||
void field_set_from_field(ProtoViewField* dst, ProtoViewField* src);
|
||||
|
||||
/* crc.c */
|
||||
uint8_t crc8(const uint8_t* data, size_t len, uint8_t init, uint8_t poly);
|
||||
uint8_t sum_bytes(const uint8_t* data, size_t len, uint8_t init);
|
||||
uint8_t xor_bytes(const uint8_t* data, size_t len, uint8_t init);
|
||||
|
||||
@@ -9,31 +9,32 @@
|
||||
#include <furi_hal_spi.h>
|
||||
#include <furi_hal_interrupt.h>
|
||||
|
||||
void raw_sampling_worker_start(ProtoViewApp* app);
|
||||
void raw_sampling_worker_stop(ProtoViewApp* app);
|
||||
void raw_sampling_timer_start(ProtoViewApp* app);
|
||||
void raw_sampling_timer_stop(ProtoViewApp* app);
|
||||
|
||||
ProtoViewModulation ProtoViewModulations[] = {
|
||||
{"OOK 650Khz", "FuriHalSubGhzPresetOok650Async", FuriHalSubGhzPresetOok650Async, NULL},
|
||||
{"OOK 270Khz", "FuriHalSubGhzPresetOok270Async", FuriHalSubGhzPresetOok270Async, NULL},
|
||||
{"OOK 650Khz", "FuriHalSubGhzPresetOok650Async", FuriHalSubGhzPresetOok650Async, NULL, 30},
|
||||
{"OOK 270Khz", "FuriHalSubGhzPresetOok270Async", FuriHalSubGhzPresetOok270Async, NULL, 30},
|
||||
{"2FSK 2.38Khz",
|
||||
"FuriHalSubGhzPreset2FSKDev238Async",
|
||||
FuriHalSubGhzPreset2FSKDev238Async,
|
||||
NULL},
|
||||
NULL,
|
||||
30},
|
||||
{"2FSK 47.6Khz",
|
||||
"FuriHalSubGhzPreset2FSKDev476Async",
|
||||
FuriHalSubGhzPreset2FSKDev476Async,
|
||||
NULL},
|
||||
{"TPMS 1 (FSK)", NULL, 0, (uint8_t*)protoview_subghz_tpms1_fsk_async_regs},
|
||||
{"TPMS 2 (OOK)", NULL, 0, (uint8_t*)protoview_subghz_tpms2_ook_async_regs},
|
||||
{"TPMS 3 (FSK)", NULL, 0, (uint8_t*)protoview_subghz_tpms3_fsk_async_regs},
|
||||
{"TPMS 4 (FSK)", NULL, 0, (uint8_t*)protoview_subghz_tpms4_fsk_async_regs},
|
||||
{NULL, NULL, 0, NULL} /* End of list sentinel. */
|
||||
NULL,
|
||||
30},
|
||||
{"TPMS 1 (FSK)", NULL, 0, (uint8_t*)protoview_subghz_tpms1_fsk_async_regs, 30},
|
||||
{"TPMS 2 (OOK)", NULL, 0, (uint8_t*)protoview_subghz_tpms2_ook_async_regs, 30},
|
||||
{"TPMS 3 (GFSK)", NULL, 0, (uint8_t*)protoview_subghz_tpms3_gfsk_async_regs, 30},
|
||||
{"OOK 40kBaud", NULL, 0, (uint8_t*)protoview_subghz_40k_ook_async_regs, 15},
|
||||
{"FSK 40kBaud", NULL, 0, (uint8_t*)protoview_subghz_40k_fsk_async_regs, 15},
|
||||
{NULL, NULL, 0, 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. */
|
||||
* subghz system and put it into idle state. */
|
||||
void radio_begin(ProtoViewApp* app) {
|
||||
furi_assert(app);
|
||||
furi_hal_subghz_reset();
|
||||
@@ -46,17 +47,28 @@ void radio_begin(ProtoViewApp* app) {
|
||||
/* 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)
|
||||
if(ProtoViewModulations[app->modulation].custom == NULL) {
|
||||
furi_hal_subghz_load_preset(ProtoViewModulations[app->modulation].preset);
|
||||
else
|
||||
} 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;
|
||||
}
|
||||
|
||||
/* ================================= Reception ============================== */
|
||||
|
||||
/* Setup subghz to start receiving using a background worker. */
|
||||
/* We avoid the subghz provided abstractions and put the data in our
|
||||
* simple abstraction: the RawSamples circular buffer. */
|
||||
void protoview_rx_callback(bool level, uint32_t duration, void* 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;
|
||||
}
|
||||
|
||||
/* Setup the CC1101 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)) {
|
||||
@@ -72,8 +84,7 @@ uint32_t radio_rx(ProtoViewApp* app) {
|
||||
furi_hal_subghz_flush_rx();
|
||||
furi_hal_subghz_rx();
|
||||
if(!app->txrx->debug_timer_sampling) {
|
||||
furi_hal_subghz_start_async_rx(subghz_worker_rx_callback, app->txrx->worker);
|
||||
subghz_worker_start(app->txrx->worker);
|
||||
furi_hal_subghz_start_async_rx(protoview_rx_callback, NULL);
|
||||
} else {
|
||||
raw_sampling_worker_start(app);
|
||||
}
|
||||
@@ -81,16 +92,13 @@ uint32_t radio_rx(ProtoViewApp* app) {
|
||||
return value;
|
||||
}
|
||||
|
||||
/* Stop subghz worker (if active), put radio on idle state. */
|
||||
/* Stop receiving (if active) and put the radio on idle state. */
|
||||
void radio_rx_end(ProtoViewApp* app) {
|
||||
furi_assert(app);
|
||||
|
||||
if(app->txrx->txrx_state == TxRxStateRx) {
|
||||
if(!app->txrx->debug_timer_sampling) {
|
||||
if(subghz_worker_is_running(app->txrx->worker)) {
|
||||
subghz_worker_stop(app->txrx->worker);
|
||||
furi_hal_subghz_stop_async_rx();
|
||||
}
|
||||
furi_hal_subghz_stop_async_rx();
|
||||
} else {
|
||||
raw_sampling_worker_stop(app);
|
||||
}
|
||||
@@ -103,8 +111,8 @@ void radio_rx_end(ProtoViewApp* app) {
|
||||
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. */
|
||||
/* Stop the asynchronous receiving system before putting the
|
||||
* chip into sleep. */
|
||||
radio_rx_end(app);
|
||||
}
|
||||
furi_hal_subghz_sleep();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 116 B After Width: | Height: | Size: 132 B |
@@ -1,3 +1,6 @@
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license. */
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
@@ -17,3 +20,17 @@ uint8_t crc8(const uint8_t* data, size_t len, uint8_t init, uint8_t poly) {
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
/* Sum all the specified bytes modulo 256.
|
||||
* Initialize the sum with 'init' (usually 0). */
|
||||
uint8_t sum_bytes(const uint8_t* data, size_t len, uint8_t init) {
|
||||
for(size_t i = 0; i < len; i++) init += data[i];
|
||||
return init;
|
||||
}
|
||||
|
||||
/* Perform the bitwise xor of all the specified bytes.
|
||||
* Initialize xor value with 'init' (usually 0). */
|
||||
uint8_t xor_bytes(const uint8_t* data, size_t len, uint8_t init) {
|
||||
for(size_t i = 0; i < len; i++) init ^= data[i];
|
||||
return init;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include <cc1101.h>
|
||||
|
||||
/* ========================== DATA RATE SETTINGS ===============================
|
||||
*
|
||||
* This is how to configure registers MDMCFG3 and MDMCFG4.
|
||||
@@ -105,7 +104,12 @@ static uint8_t protoview_subghz_tpms1_fsk_async_regs[][2] = {
|
||||
|
||||
/* End */
|
||||
{0, 0},
|
||||
};
|
||||
|
||||
/* CC1101 2FSK PATABLE. */
|
||||
{0xC0, 0},
|
||||
{0, 0},
|
||||
{0, 0},
|
||||
{0, 0}};
|
||||
|
||||
/* This is like the default Flipper OOK 640Khz bandwidth preset, but
|
||||
* the bandwidth is changed to 10kBaud to accomodate TPMS frequency. */
|
||||
@@ -152,10 +156,64 @@ static const uint8_t protoview_subghz_tpms2_ook_async_regs[][2] = {
|
||||
|
||||
/* End */
|
||||
{0, 0},
|
||||
};
|
||||
|
||||
/* CC1101 OOK PATABLE. */
|
||||
{0, 0xC0},
|
||||
{0, 0},
|
||||
{0, 0},
|
||||
{0, 0}};
|
||||
|
||||
/* GFSK 19k dev, 325 Khz filter, 20kBaud. Different AGI settings.
|
||||
* Works well with Toyota. */
|
||||
static uint8_t protoview_subghz_tpms3_gfsk_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}, // 2 is the channel spacing exponet: not used
|
||||
{CC1101_MDMCFG2, 0x10}, // GFSK without any other check
|
||||
{CC1101_MDMCFG3, 0x93}, // Data rate is 20kBaud
|
||||
{CC1101_MDMCFG4, 0x59}, // Rx bandwidth filter is 325 kHz
|
||||
{CC1101_DEVIATN, 0x34}, // Deviation 19.04 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, 0x80},
|
||||
{CC1101_AGCCTRL1, 0x58},
|
||||
{CC1101_AGCCTRL2, 0x87},
|
||||
|
||||
/* 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},
|
||||
|
||||
/* CC1101 2FSK PATABLE. */
|
||||
{0xC0, 0},
|
||||
{0, 0},
|
||||
{0, 0},
|
||||
{0, 0}};
|
||||
|
||||
/* 40 KBaud, 2FSK, 28 kHz deviation, 270 Khz bandwidth filter. */
|
||||
static uint8_t protoview_subghz_tpms3_fsk_async_regs[][2] = {
|
||||
static uint8_t protoview_subghz_40k_fsk_async_regs[][2] = {
|
||||
/* GPIO GD0 */
|
||||
{CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input
|
||||
|
||||
@@ -198,47 +256,62 @@ static uint8_t protoview_subghz_tpms3_fsk_async_regs[][2] = {
|
||||
|
||||
/* End */
|
||||
{0, 0},
|
||||
};
|
||||
|
||||
/* FSK 19k dev, 325 Khz filter, 20kBaud. Works well with Toyota. */
|
||||
static uint8_t protoview_subghz_tpms4_fsk_async_regs[][2] = {
|
||||
/* CC1101 2FSK PATABLE. */
|
||||
{0xC0, 0},
|
||||
{0, 0},
|
||||
{0, 0},
|
||||
{0, 0}};
|
||||
|
||||
/* This is like the default Flipper OOK 640Khz bandwidth preset, but
|
||||
* the bandwidth is changed to 40kBaud, in order to receive signals
|
||||
* with a pulse width ~25us/30us. */
|
||||
static const uint8_t protoview_subghz_40k_ook_async_regs[][2] = {
|
||||
/* GPIO GD0 */
|
||||
{CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input
|
||||
|
||||
/* FIFO and internals */
|
||||
{CC1101_FIFOTHR, 0x07}, // The only important bit is ADC_RETENTION
|
||||
|
||||
/* Packet engine */
|
||||
{CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening
|
||||
|
||||
/* 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}, // 2 is the channel spacing exponet: not used
|
||||
{CC1101_MDMCFG2, 0x10}, // GFSK without any other check
|
||||
{CC1101_MDMCFG3, 0x93}, // Data rate is 20kBaud
|
||||
{CC1101_MDMCFG4, 0x59}, // Rx bandwidth filter is 325 kHz
|
||||
{CC1101_DEVIATN, 0x34}, // Deviation 19.04 Khz.
|
||||
// Modem Configuration
|
||||
{CC1101_MDMCFG0, 0x00}, // Channel spacing is 25kHz
|
||||
{CC1101_MDMCFG1, 0x00}, // Channel spacing is 25kHz
|
||||
{CC1101_MDMCFG2, 0x30}, // Format ASK/OOK, No preamble/sync
|
||||
{CC1101_MDMCFG3, 0x93}, // Data rate is 40kBaud
|
||||
{CC1101_MDMCFG4, 0x1A}, // Rx BW filter is 650.000kHz
|
||||
|
||||
/* 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
|
||||
0x18}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off
|
||||
|
||||
/* Automatic Gain Control */
|
||||
{CC1101_AGCCTRL0, 0x80},
|
||||
{CC1101_AGCCTRL1, 0x58},
|
||||
{CC1101_AGCCTRL2, 0x87},
|
||||
{CC1101_AGCCTRL0,
|
||||
0x91}, // 10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary
|
||||
{CC1101_AGCCTRL1,
|
||||
0x0}, // 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},
|
||||
{CC1101_FREND0, 0x11}, // Adjusts current TX LO buffer + high is PATABLE[1]
|
||||
{CC1101_FREND1, 0xB6}, //
|
||||
|
||||
/* End */
|
||||
{0, 0},
|
||||
};
|
||||
|
||||
/* CC1101 OOK PATABLE. */
|
||||
{0, 0xC0},
|
||||
{0, 0},
|
||||
{0, 0},
|
||||
{0, 0}};
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/* 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)};
|
||||
369
applications/plugins/protoview/fields.c
Normal file
369
applications/plugins/protoview/fields.c
Normal file
@@ -0,0 +1,369 @@
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* Protocol fields implementation. */
|
||||
|
||||
#include "app.h"
|
||||
|
||||
/* Create a new field of the specified type. Without populating its
|
||||
* type-specific value. */
|
||||
static ProtoViewField* field_new(ProtoViewFieldType type, const char* name) {
|
||||
ProtoViewField* f = malloc(sizeof(*f));
|
||||
f->type = type;
|
||||
f->name = strdup(name);
|
||||
return f;
|
||||
}
|
||||
|
||||
/* Free only the auxiliary data of a field, used to represent the
|
||||
* current type. Name and type are not touched. */
|
||||
static void field_free_aux_data(ProtoViewField* f) {
|
||||
switch(f->type) {
|
||||
case FieldTypeStr:
|
||||
free(f->str);
|
||||
break;
|
||||
case FieldTypeBytes:
|
||||
free(f->bytes);
|
||||
break;
|
||||
default:
|
||||
break; // Nothing to free for other types.
|
||||
}
|
||||
}
|
||||
|
||||
/* Free a field an associated data. */
|
||||
static void field_free(ProtoViewField* f) {
|
||||
field_free_aux_data(f);
|
||||
free(f->name);
|
||||
free(f);
|
||||
}
|
||||
|
||||
/* Return the type of the field as string. */
|
||||
const char* field_get_type_name(ProtoViewField* f) {
|
||||
switch(f->type) {
|
||||
case FieldTypeStr:
|
||||
return "str";
|
||||
case FieldTypeSignedInt:
|
||||
return "int";
|
||||
case FieldTypeUnsignedInt:
|
||||
return "uint";
|
||||
case FieldTypeBinary:
|
||||
return "bin";
|
||||
case FieldTypeHex:
|
||||
return "hex";
|
||||
case FieldTypeBytes:
|
||||
return "bytes";
|
||||
case FieldTypeFloat:
|
||||
return "float";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/* Set a string representation of the specified field in buf. */
|
||||
int field_to_string(char* buf, size_t len, ProtoViewField* f) {
|
||||
switch(f->type) {
|
||||
case FieldTypeStr:
|
||||
return snprintf(buf, len, "%s", f->str);
|
||||
case FieldTypeSignedInt:
|
||||
return snprintf(buf, len, "%lld", (long long)f->value);
|
||||
case FieldTypeUnsignedInt:
|
||||
return snprintf(buf, len, "%llu", (unsigned long long)f->uvalue);
|
||||
case FieldTypeBinary: {
|
||||
uint64_t test_bit = (1 << (f->len - 1));
|
||||
uint64_t idx = 0;
|
||||
while(idx < len - 1 && test_bit) {
|
||||
buf[idx++] = (f->uvalue & test_bit) ? '1' : '0';
|
||||
test_bit >>= 1;
|
||||
}
|
||||
buf[idx] = 0;
|
||||
return idx;
|
||||
}
|
||||
case FieldTypeHex:
|
||||
return snprintf(buf, len, "%*llX", (int)(f->len + 7) / 8, f->uvalue);
|
||||
case FieldTypeFloat:
|
||||
return snprintf(buf, len, "%.*f", (int)f->len, (double)f->fvalue);
|
||||
case FieldTypeBytes: {
|
||||
uint64_t idx = 0;
|
||||
while(idx < len - 1 && idx < f->len) {
|
||||
const char* charset = "0123456789ABCDEF";
|
||||
uint32_t nibble = idx & 1 ? (f->bytes[idx / 2] & 0xf) : (f->bytes[idx / 2] >> 4);
|
||||
buf[idx++] = charset[nibble];
|
||||
}
|
||||
buf[idx] = 0;
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Set the field value from its string representation in 'buf'.
|
||||
* The field type must already be set and the field should be valid.
|
||||
* The string represenation 'buf' must be null termianted. Note that
|
||||
* even when representing binary values containing zero, this values
|
||||
* are taken as representations, so that would be the string "00" as
|
||||
* the Bytes type representation.
|
||||
*
|
||||
* The function returns true if the filed was successfully set to the
|
||||
* new value, otherwise if the specified value is invalid for the
|
||||
* field type, false is returned. */
|
||||
bool field_set_from_string(ProtoViewField* f, char* buf, size_t len) {
|
||||
// Initialize values to zero since the Flipper sscanf() implementation
|
||||
// is fuzzy... may populate only part of the value.
|
||||
long long val = 0;
|
||||
unsigned long long uval = 0;
|
||||
float fval = 0;
|
||||
|
||||
switch(f->type) {
|
||||
case FieldTypeStr:
|
||||
free(f->str);
|
||||
f->len = len;
|
||||
f->str = malloc(len + 1);
|
||||
memcpy(f->str, buf, len + 1);
|
||||
break;
|
||||
case FieldTypeSignedInt:
|
||||
if(!sscanf(buf, "%lld", &val)) return false;
|
||||
f->value = val;
|
||||
break;
|
||||
case FieldTypeUnsignedInt:
|
||||
if(!sscanf(buf, "%llu", &uval)) return false;
|
||||
f->uvalue = uval;
|
||||
break;
|
||||
case FieldTypeBinary: {
|
||||
uint64_t bit_to_set = (1 << (len - 1));
|
||||
uint64_t idx = 0;
|
||||
uval = 0;
|
||||
while(buf[idx]) {
|
||||
if(buf[idx] == '1')
|
||||
uval |= bit_to_set;
|
||||
else if(buf[idx] != '0')
|
||||
return false;
|
||||
bit_to_set >>= 1;
|
||||
idx++;
|
||||
}
|
||||
f->uvalue = uval;
|
||||
} break;
|
||||
case FieldTypeHex:
|
||||
if(!sscanf(buf, "%llx", &uval) && !sscanf(buf, "%llX", &uval)) return false;
|
||||
f->uvalue = uval;
|
||||
break;
|
||||
case FieldTypeFloat:
|
||||
if(!sscanf(buf, "%f", &fval)) return false;
|
||||
f->fvalue = fval;
|
||||
break;
|
||||
case FieldTypeBytes: {
|
||||
if(len > f->len) return false;
|
||||
uint64_t idx = 0;
|
||||
while(buf[idx]) {
|
||||
uint8_t nibble = 0;
|
||||
char c = toupper(buf[idx]);
|
||||
if(c >= '0' && c <= '9')
|
||||
nibble = c - '0';
|
||||
else if(c >= 'A' && c <= 'F')
|
||||
nibble = 10 + (c - 'A');
|
||||
else
|
||||
return false;
|
||||
|
||||
if(idx & 1) {
|
||||
f->bytes[idx / 2] = (f->bytes[idx / 2] & 0xF0) | nibble;
|
||||
} else {
|
||||
f->bytes[idx / 2] = (f->bytes[idx / 2] & 0x0F) | (nibble << 4);
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
buf[idx] = 0;
|
||||
} break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Set the 'dst' field to contain a copy of the value of the 'src'
|
||||
* field. The field name is not modified. */
|
||||
void field_set_from_field(ProtoViewField* dst, ProtoViewField* src) {
|
||||
field_free_aux_data(dst);
|
||||
dst->type = src->type;
|
||||
dst->len = src->len;
|
||||
switch(src->type) {
|
||||
case FieldTypeStr:
|
||||
dst->str = strdup(src->str);
|
||||
break;
|
||||
case FieldTypeBytes:
|
||||
dst->bytes = malloc(src->len);
|
||||
memcpy(dst->bytes, src->bytes, dst->len);
|
||||
break;
|
||||
case FieldTypeSignedInt:
|
||||
dst->value = src->value;
|
||||
break;
|
||||
case FieldTypeUnsignedInt:
|
||||
case FieldTypeBinary:
|
||||
case FieldTypeHex:
|
||||
dst->uvalue = src->uvalue;
|
||||
break;
|
||||
case FieldTypeFloat:
|
||||
dst->fvalue = src->fvalue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Increment the specified field value of 'incr'. If the field type
|
||||
* does not support increments false is returned, otherwise the
|
||||
* action is performed. */
|
||||
bool field_incr_value(ProtoViewField* f, int incr) {
|
||||
switch(f->type) {
|
||||
case FieldTypeStr:
|
||||
return false;
|
||||
case FieldTypeSignedInt: {
|
||||
/* Wrap around depending on the number of bits (f->len)
|
||||
* the integer was declared to have. */
|
||||
int64_t max = (1ULL << (f->len - 1)) - 1;
|
||||
int64_t min = -max - 1;
|
||||
int64_t v = (int64_t)f->value + incr;
|
||||
if(v > max) v = min + (v - max - 1);
|
||||
if(v < min) v = max + (v - min + 1);
|
||||
f->value = v;
|
||||
break;
|
||||
}
|
||||
case FieldTypeBinary:
|
||||
case FieldTypeHex:
|
||||
case FieldTypeUnsignedInt: {
|
||||
/* Wrap around like for the unsigned case, but here
|
||||
* is simpler. */
|
||||
uint64_t max = (1ULL << f->len) - 1; // Broken for 64 bits.
|
||||
uint64_t uv = (uint64_t)f->value + incr;
|
||||
if(uv > max) uv = uv & max;
|
||||
f->uvalue = uv;
|
||||
break;
|
||||
}
|
||||
case FieldTypeFloat:
|
||||
f->fvalue += incr;
|
||||
break;
|
||||
case FieldTypeBytes: {
|
||||
// For bytes we only support single unit increments.
|
||||
if(incr != -1 && incr != 1) return false;
|
||||
for(int j = f->len - 1; j >= 0; j--) {
|
||||
uint8_t nibble = (j & 1) ? (f->bytes[j / 2] & 0x0F) : ((f->bytes[j / 2] & 0xF0) >> 4);
|
||||
|
||||
nibble += incr;
|
||||
nibble &= 0x0F;
|
||||
|
||||
f->bytes[j / 2] = (j & 1) ? ((f->bytes[j / 2] & 0xF0) | nibble) :
|
||||
((f->bytes[j / 2] & 0x0F) | (nibble << 4));
|
||||
|
||||
/* Propagate the operation on overflow of this nibble. */
|
||||
if((incr == 1 && nibble == 0) || (incr == -1 && nibble == 0xf)) {
|
||||
continue;
|
||||
}
|
||||
break; // Otherwise stop the loop here.
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Free a field set and its contained fields. */
|
||||
void fieldset_free(ProtoViewFieldSet* fs) {
|
||||
for(uint32_t j = 0; j < fs->numfields; j++) field_free(fs->fields[j]);
|
||||
free(fs->fields);
|
||||
free(fs);
|
||||
}
|
||||
|
||||
/* Allocate and init an empty field set. */
|
||||
ProtoViewFieldSet* fieldset_new(void) {
|
||||
ProtoViewFieldSet* fs = malloc(sizeof(*fs));
|
||||
fs->numfields = 0;
|
||||
fs->fields = NULL;
|
||||
return fs;
|
||||
}
|
||||
|
||||
/* Append an already allocated field at the end of the specified field set. */
|
||||
static void fieldset_add_field(ProtoViewFieldSet* fs, ProtoViewField* field) {
|
||||
fs->numfields++;
|
||||
fs->fields = realloc(fs->fields, sizeof(ProtoViewField*) * fs->numfields);
|
||||
fs->fields[fs->numfields - 1] = field;
|
||||
}
|
||||
|
||||
/* Allocate and append an integer field. */
|
||||
void fieldset_add_int(ProtoViewFieldSet* fs, const char* name, int64_t val, uint8_t bits) {
|
||||
ProtoViewField* f = field_new(FieldTypeSignedInt, name);
|
||||
f->value = val;
|
||||
f->len = bits;
|
||||
fieldset_add_field(fs, f);
|
||||
}
|
||||
|
||||
/* Allocate and append an unsigned field. */
|
||||
void fieldset_add_uint(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits) {
|
||||
ProtoViewField* f = field_new(FieldTypeUnsignedInt, name);
|
||||
f->uvalue = uval;
|
||||
f->len = bits;
|
||||
fieldset_add_field(fs, f);
|
||||
}
|
||||
|
||||
/* Allocate and append a hex field. This is an unsigned number but
|
||||
* with an hex representation. */
|
||||
void fieldset_add_hex(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits) {
|
||||
ProtoViewField* f = field_new(FieldTypeHex, name);
|
||||
f->uvalue = uval;
|
||||
f->len = bits;
|
||||
fieldset_add_field(fs, f);
|
||||
}
|
||||
|
||||
/* Allocate and append a bin field. This is an unsigned number but
|
||||
* with a binary representation. */
|
||||
void fieldset_add_bin(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits) {
|
||||
ProtoViewField* f = field_new(FieldTypeBinary, name);
|
||||
f->uvalue = uval;
|
||||
f->len = bits;
|
||||
fieldset_add_field(fs, f);
|
||||
}
|
||||
|
||||
/* Allocate and append a string field. The string 's' does not need to point
|
||||
* to a null terminated string, but must have at least 'len' valid bytes
|
||||
* starting from the pointer. The field object will be correctly null
|
||||
* terminated. */
|
||||
void fieldset_add_str(ProtoViewFieldSet* fs, const char* name, const char* s, size_t len) {
|
||||
ProtoViewField* f = field_new(FieldTypeStr, name);
|
||||
f->len = len;
|
||||
f->str = malloc(len + 1);
|
||||
memcpy(f->str, s, len);
|
||||
f->str[len] = 0;
|
||||
fieldset_add_field(fs, f);
|
||||
}
|
||||
|
||||
/* Allocate and append a bytes field. Note that 'count' is specified in
|
||||
* nibbles (bytes*2). */
|
||||
void fieldset_add_bytes(
|
||||
ProtoViewFieldSet* fs,
|
||||
const char* name,
|
||||
const uint8_t* bytes,
|
||||
uint32_t count_nibbles) {
|
||||
uint32_t numbytes = (count_nibbles + count_nibbles % 2) / 2;
|
||||
ProtoViewField* f = field_new(FieldTypeBytes, name);
|
||||
f->bytes = malloc(numbytes);
|
||||
memcpy(f->bytes, bytes, numbytes);
|
||||
f->len = count_nibbles;
|
||||
fieldset_add_field(fs, f);
|
||||
}
|
||||
|
||||
/* Allocate and append a float field. */
|
||||
void fieldset_add_float(
|
||||
ProtoViewFieldSet* fs,
|
||||
const char* name,
|
||||
float val,
|
||||
uint32_t digits_after_dot) {
|
||||
ProtoViewField* f = field_new(FieldTypeFloat, name);
|
||||
f->fvalue = val;
|
||||
f->len = digits_after_dot;
|
||||
fieldset_add_field(fs, f);
|
||||
}
|
||||
|
||||
/* For each field of the destination filedset 'dst', look for a matching
|
||||
* field name/type in the source fieldset 'src', and if one is found copy
|
||||
* its value into the 'dst' field. */
|
||||
void fieldset_copy_matching_fields(ProtoViewFieldSet* dst, ProtoViewFieldSet* src) {
|
||||
for(uint32_t j = 0; j < dst->numfields; j++) {
|
||||
for(uint32_t i = 0; i < src->numfields; i++) {
|
||||
if(dst->fields[j]->type == src->fields[i]->type &&
|
||||
!strcmp(dst->fields[j]->name, src->fields[i]->name)) {
|
||||
field_set_from_field(dst->fields[j], src->fields[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
/* PT/SC remotes. Usually 443.92 Mhz OOK.
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* PT/SC remotes. Usually 443.92 Mhz OOK.
|
||||
*
|
||||
* This line code is used in many remotes such as Princeton chips
|
||||
* named PT2262, Silian Microelectronics SC5262 and others.
|
||||
@@ -11,10 +14,15 @@
|
||||
|
||||
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. */
|
||||
|
||||
/* Test different pulse + gap + first byte possibilities. */
|
||||
const char* sync_patterns[6] = {
|
||||
"100000000000000000000000000000011101", /* 30 times gap + one. */
|
||||
"100000000000000000000000000000010001", /* 30 times gap + zero. */
|
||||
"1000000000000000000000000000000011101", /* 31 times gap + one. */
|
||||
"1000000000000000000000000000000010001", /* 31 times gap + zero. */
|
||||
"10000000000000000000000000000000011101", /* 32 times gap + one. */
|
||||
"10000000000000000000000000000000010001", /* 32 times gap + zero. */
|
||||
};
|
||||
|
||||
uint32_t off;
|
||||
@@ -24,11 +32,11 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
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);
|
||||
if(DEBUG_MSG) FURI_LOG_E(TAG, "B4B1 preamble id:%d at: %lu", j, off);
|
||||
info->start_off = off;
|
||||
|
||||
// Seek data setction. Why -1? Last bit is data.
|
||||
off += strlen(sync_patterns[j]) - 1;
|
||||
// Seek data setction. Why -5? Last 5 half-bit-times are data.
|
||||
off += strlen(sync_patterns[j]) - 5;
|
||||
|
||||
uint8_t d[3]; /* 24 bits of data. */
|
||||
uint32_t decoded = convert_from_line_code(d, sizeof(d), bits, numbytes, off, "1000", "1110");
|
||||
@@ -39,9 +47,47 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
off += 24 * 4; // seek to end symbol offset to calculate the length.
|
||||
off++; // In this protocol there is a final pulse as terminator.
|
||||
info->pulses_count = off - info->start_off;
|
||||
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]);
|
||||
|
||||
fieldset_add_bytes(info->fieldset, "id", d, 5);
|
||||
fieldset_add_uint(info->fieldset, "button", d[2] & 0xf, 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoViewDecoder B4B1Decoder = {"B4B1", decode};
|
||||
/* Give fields and defaults for the signal creator. */
|
||||
static void get_fields(ProtoViewFieldSet* fieldset) {
|
||||
uint8_t default_id[3] = {0xAB, 0xCD, 0xE0};
|
||||
fieldset_add_bytes(fieldset, "id", default_id, 5);
|
||||
fieldset_add_uint(fieldset, "button", 1, 4);
|
||||
}
|
||||
|
||||
/* Create a signal. */
|
||||
static void build_message(RawSamplesBuffer* samples, ProtoViewFieldSet* fs) {
|
||||
uint32_t te = 334; // Short pulse duration in microseconds.
|
||||
|
||||
// Sync: 1 te pulse, 31 te gap.
|
||||
raw_samples_add(samples, true, te);
|
||||
raw_samples_add(samples, false, te * 31);
|
||||
|
||||
// ID + button state
|
||||
uint8_t data[3];
|
||||
memcpy(data, fs->fields[0]->bytes, 3);
|
||||
data[2] = (data[2] & 0xF0) | (fs->fields[1]->uvalue & 0xF);
|
||||
for(uint32_t j = 0; j < 24; j++) {
|
||||
if(bitmap_get(data, sizeof(data), j)) {
|
||||
raw_samples_add(samples, true, te * 3);
|
||||
raw_samples_add(samples, false, te);
|
||||
} else {
|
||||
raw_samples_add(samples, true, te);
|
||||
raw_samples_add(samples, false, te * 3);
|
||||
}
|
||||
}
|
||||
|
||||
// Signal terminator. Just a single short pulse.
|
||||
raw_samples_add(samples, true, te);
|
||||
}
|
||||
|
||||
ProtoViewDecoder B4B1Decoder = {
|
||||
.name = "PT/SC remote",
|
||||
.decode = decode,
|
||||
.get_fields = get_fields,
|
||||
.build_message = build_message};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* Microchip HCS200/HCS300/HSC301 KeeLoq, rolling code remotes.
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* Microchip HCS200/HCS300/HSC301 KeeLoq, rolling code remotes.
|
||||
*
|
||||
* Usually 443.92 Mhz OOK, ~200us or ~400us pulse len, depending
|
||||
* on the configuration.
|
||||
@@ -56,43 +59,63 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
|
||||
info->pulses_count = (off + 66 * 3) - info->start_off;
|
||||
|
||||
bitmap_reverse_bytes(raw, sizeof(raw)); /* Keeloq is LSB first. */
|
||||
bitmap_reverse_bytes_bits(raw, sizeof(raw)); /* Keeloq is LSB first. */
|
||||
|
||||
int buttons = raw[7] >> 4;
|
||||
int s3 = (buttons & 1) != 0;
|
||||
int s0 = (buttons & 2) != 0;
|
||||
int s1 = (buttons & 4) != 0;
|
||||
int s2 = (buttons & 8) != 0;
|
||||
int lowbat = (raw[8] & 0x1) == 0; // Actual bit meaning: good battery level
|
||||
int alwaysone = (raw[8] & 0x2) != 0;
|
||||
|
||||
int remote_id = ((raw[7] & 0x0f) << 24) | (raw[6] << 16) | (raw[5] << 8) | (raw[4] << 0);
|
||||
int lowbat = raw[8] & 0x80;
|
||||
|
||||
snprintf(info->name, sizeof(info->name), "%s", "Keeloq remote");
|
||||
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),
|
||||
"Encrpyted %02X%02X%02X%02X",
|
||||
raw[3],
|
||||
raw[2],
|
||||
raw[1],
|
||||
raw[0]);
|
||||
snprintf(info->info2, sizeof(info->info2), "ID %08X", remote_id);
|
||||
snprintf(info->info3, sizeof(info->info3), "s0-s3: %d%d%d%d", s0, s1, s2, s3);
|
||||
snprintf(info->info4, sizeof(info->info4), "Low battery? %s", lowbat ? "yes" : "no");
|
||||
fieldset_add_bytes(info->fieldset, "encr", raw, 8);
|
||||
raw[7] = raw[7] << 4; // Make ID bits contiguous
|
||||
fieldset_add_bytes(info->fieldset, "id", raw + 4, 7); // 28 bits, 7 nibbles
|
||||
fieldset_add_bin(info->fieldset, "s[2,1,0,3]", buttons, 4);
|
||||
fieldset_add_bin(info->fieldset, "low battery", lowbat, 1);
|
||||
fieldset_add_bin(info->fieldset, "always one", alwaysone, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoViewDecoder KeeloqDecoder = {"Keeloq", decode};
|
||||
static void get_fields(ProtoViewFieldSet* fieldset) {
|
||||
uint8_t remote_id[4] = {0xab, 0xcd, 0xef, 0xa0};
|
||||
uint8_t encr[4] = {0xab, 0xab, 0xab, 0xab};
|
||||
fieldset_add_bytes(fieldset, "encr", encr, 8);
|
||||
fieldset_add_bytes(fieldset, "id", remote_id, 7);
|
||||
fieldset_add_bin(fieldset, "s[2,1,0,3]", 2, 4);
|
||||
fieldset_add_bin(fieldset, "low battery", 0, 1);
|
||||
fieldset_add_bin(fieldset, "always one", 1, 1);
|
||||
}
|
||||
|
||||
static void build_message(RawSamplesBuffer* samples, ProtoViewFieldSet* fieldset) {
|
||||
uint32_t te = 380; // Short pulse duration in microseconds.
|
||||
|
||||
// Sync: 12 pairs of pulse/gap + 9 times gap
|
||||
for(int j = 0; j < 12; j++) {
|
||||
raw_samples_add(samples, true, te);
|
||||
raw_samples_add(samples, false, te);
|
||||
}
|
||||
raw_samples_add(samples, false, te * 9);
|
||||
|
||||
// Data, 66 bits.
|
||||
uint8_t data[9] = {0};
|
||||
memcpy(data, fieldset->fields[0]->bytes, 4); // Encrypted part.
|
||||
memcpy(data + 4, fieldset->fields[1]->bytes, 4); // ID.
|
||||
data[7] = data[7] >> 4 | fieldset->fields[2]->uvalue << 4; // s[2,1,0,3]
|
||||
int low_battery = fieldset->fields[3] != 0;
|
||||
int always_one = fieldset->fields[4] != 0;
|
||||
low_battery = !low_battery; // Bit real meaning is good battery level.
|
||||
data[8] |= low_battery;
|
||||
data[8] |= (always_one << 1);
|
||||
bitmap_reverse_bytes_bits(data, sizeof(data)); /* Keeloq is LSB first. */
|
||||
|
||||
for(int j = 0; j < 66; j++) {
|
||||
if(bitmap_get(data, 9, j)) {
|
||||
raw_samples_add(samples, true, te);
|
||||
raw_samples_add(samples, false, te * 2);
|
||||
} else {
|
||||
raw_samples_add(samples, true, te * 2);
|
||||
raw_samples_add(samples, false, te);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProtoViewDecoder KeeloqDecoder =
|
||||
{.name = "Keeloq", .decode = decode, .get_fields = get_fields, .build_message = build_message};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* Oregon remote termometers. Usually 443.92 Mhz OOK.
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* Oregon remote termometers. Usually 443.92 Mhz OOK.
|
||||
*
|
||||
* The protocol is described here:
|
||||
* https://wmrx00.sourceforge.net/Arduino/OregonScientific-RF-Protocols.pdf
|
||||
@@ -27,7 +30,8 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
if(decoded < 11 * 4) return false; /* Minimum len to extract some data. */
|
||||
info->pulses_count = (off + 11 * 4 * 4) - info->start_off;
|
||||
|
||||
char temp[3] = {0}, deviceid[2] = {0}, hum[2] = {0};
|
||||
char temp[3] = {0}, hum[2] = {0};
|
||||
uint8_t deviceid[2];
|
||||
for(int j = 0; j < 64; j += 4) {
|
||||
uint8_t nib[1];
|
||||
nib[0] =
|
||||
@@ -67,25 +71,14 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
float tempval = ((temp[0] - '0') * 10) + (temp[1] - '0') + ((float)(temp[2] - '0') * 0.1);
|
||||
int humval = (hum[0] - '0') * 10 + (hum[1] - '0');
|
||||
|
||||
fieldset_add_bytes(info->fieldset, "Sensor ID", deviceid, 4);
|
||||
fieldset_add_float(info->fieldset, "Temperature", tempval, 1);
|
||||
fieldset_add_uint(info->fieldset, "Humidity", humval, 7);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoViewDecoder Oregon2Decoder = {"Oregon2", decode};
|
||||
ProtoViewDecoder Oregon2Decoder =
|
||||
{.name = "Oregon2", .decode = decode, .get_fields = NULL, .build_message = NULL};
|
||||
|
||||
205
applications/plugins/protoview/protocols/pvchat.c
Normal file
205
applications/plugins/protoview/protocols/pvchat.c
Normal file
@@ -0,0 +1,205 @@
|
||||
#include "../app.h"
|
||||
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* ----------------------------------------------------------
|
||||
* ProtoView chat protocol. This is just a fun test protocol
|
||||
* that can be used between two Flippers in order to send
|
||||
* and receive text messages.
|
||||
* ----------------------------------------------------------
|
||||
*
|
||||
* Protocol description
|
||||
* ====================
|
||||
*
|
||||
* The protocol works with different data rates. However here is defined
|
||||
* to use a short pulse/gap duration of 300us and a long pulse/gap
|
||||
* duration of 600us. Even with the Flipper hardware, the protocol works
|
||||
* with 100/200us, but becomes less reliable and standard presets can't
|
||||
* be used because of the higher data rate.
|
||||
*
|
||||
* In the following description we have that:
|
||||
*
|
||||
* "1" represents a pulse of one-third bit time (300us)
|
||||
* "0" represents a gap of one-third bit time (300us)
|
||||
*
|
||||
* The message starts with a preamble + a sync pattern:
|
||||
*
|
||||
* preamble = 1010101010101010 x 3
|
||||
* sync = 1100110011001010
|
||||
*
|
||||
* The a variable amount of bytes follow, where each bit
|
||||
* is encoded in the following way:
|
||||
*
|
||||
* zero 100 (300 us pulse, 600 us gap)
|
||||
* one 110 (600 us pulse, 300 us gap)
|
||||
*
|
||||
* Bytes are sent MSB first, so receiving, in sequence, bits
|
||||
* 11100001, means byte E1.
|
||||
*
|
||||
* This is the data format:
|
||||
*
|
||||
* +--+------+-------+--+--+--+
|
||||
* |SL|Sender|Message|FF|AA|CS|
|
||||
* +--+------+-------+--+--+--+
|
||||
* | | |
|
||||
* | | \_ N bytes of message terminated by FF AA + 1 byte of checksum
|
||||
* | |
|
||||
* | \_ SL bytes of sender name
|
||||
* \
|
||||
* \_ 1 byte of sender len, 8 bit unsigned integer.
|
||||
*
|
||||
*
|
||||
* Checksum = sum of bytes modulo 256, with checksum set
|
||||
* to 0 for the computation.
|
||||
*
|
||||
* Design notes
|
||||
* ============
|
||||
*
|
||||
* The protocol is designed in order to have certain properties:
|
||||
*
|
||||
* 1. Pulses and gaps can only be 100 or 200 microseconds, so the
|
||||
* message can be described, encoded and decoded with only two
|
||||
* fixed durations.
|
||||
*
|
||||
* 2. The preamble + sync is designed to have a well recognizable
|
||||
* pattern that can't be reproduced just for accident inside
|
||||
* the encoded pattern. There is no combinatio of encoded bits
|
||||
* leading to the preamble+sync. Also the sync pattern final
|
||||
* part can't be mistaken for actual bits of data, since it
|
||||
* contains alternating short pulses/gaps at 100us.
|
||||
*
|
||||
* 3. Data encoding wastes some bandwidth in order to be more
|
||||
* robust. Even so, with a 300us clock period, a single bit
|
||||
* bit takes 900us, reaching a data transfer of 138 characters per
|
||||
* second. More than enough for the simple chat we have here.
|
||||
*/
|
||||
|
||||
static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
|
||||
const char* sync_pattern = "1010101010101010" // Preamble
|
||||
"1100110011001010"; // Sync
|
||||
uint8_t sync_len = 32;
|
||||
|
||||
/* This is a variable length message, however the minimum length
|
||||
* requires a sender len byte (of value zero) and the terminator
|
||||
* FF 00 plus checksum: a total of 4 bytes. */
|
||||
if(numbits - sync_len < 8 * 4) return false;
|
||||
|
||||
uint64_t off = bitmap_seek_bits(bits, numbytes, 0, numbits, sync_pattern);
|
||||
if(off == BITMAP_SEEK_NOT_FOUND) return false;
|
||||
FURI_LOG_E(TAG, "Chat preamble+sync found");
|
||||
|
||||
/* If there is room on the left, let's mark the start of the message
|
||||
* a bit before: we don't try to detect all the preamble, but only
|
||||
* the first part, however it is likely present. */
|
||||
if(off >= 16) {
|
||||
off -= 16;
|
||||
sync_len += 16;
|
||||
}
|
||||
|
||||
info->start_off = off;
|
||||
off += sync_len; /* Skip preamble and sync. */
|
||||
|
||||
uint8_t raw[64] = {(uint8_t)'.'};
|
||||
uint32_t decoded =
|
||||
convert_from_line_code(raw, sizeof(raw), bits, numbytes, off, "100", "110"); /* PWM */
|
||||
FURI_LOG_E(TAG, "Chat decoded bits: %lu", decoded);
|
||||
|
||||
if(decoded < 8 * 4) return false; /* Min message len. */
|
||||
|
||||
// The message needs to have a two bytes terminator before
|
||||
// the checksum.
|
||||
uint32_t j;
|
||||
for(j = 0; j < sizeof(raw) - 1; j++)
|
||||
if(raw[j] == 0xff && raw[j + 1] == 0xaa) break;
|
||||
|
||||
if(j == sizeof(raw) - 1) {
|
||||
FURI_LOG_E(TAG, "Chat: terminator not found");
|
||||
return false; // No terminator found.
|
||||
}
|
||||
|
||||
uint32_t datalen = j + 3; // If the terminator was found at j, then
|
||||
// we need to sum three more bytes to have
|
||||
// the len: FF itself, AA, checksum.
|
||||
info->pulses_count = sync_len + 8 * 3 * datalen;
|
||||
|
||||
// Check if the control sum matches.
|
||||
if(sum_bytes(raw, datalen - 1, 0) != raw[datalen - 1]) {
|
||||
FURI_LOG_E(TAG, "Chat: checksum mismatch");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the length of the sender looks sane
|
||||
uint8_t senderlen = raw[0];
|
||||
if(senderlen >= sizeof(raw)) {
|
||||
FURI_LOG_E(TAG, "Chat: invalid sender length");
|
||||
return false; // Overflow
|
||||
}
|
||||
|
||||
fieldset_add_str(info->fieldset, "sender", (char*)raw + 1, senderlen);
|
||||
fieldset_add_str(
|
||||
info->fieldset, "message", (char*)raw + 1 + senderlen, datalen - senderlen - 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Give fields and defaults for the signal creator. */
|
||||
static void get_fields(ProtoViewFieldSet* fieldset) {
|
||||
fieldset_add_str(fieldset, "sender", "Carol", 5);
|
||||
fieldset_add_str(fieldset, "message", "Anyone hearing?", 15);
|
||||
}
|
||||
|
||||
/* Create a signal. */
|
||||
static void build_message(RawSamplesBuffer* samples, ProtoViewFieldSet* fs) {
|
||||
uint32_t te = 300; /* Short pulse duration in microseconds.
|
||||
Our protocol needs three symbol times to send
|
||||
a bit, so 300 us per bit = 3.33 kBaud. */
|
||||
|
||||
// Preamble: 24 alternating 300us pulse/gap pairs.
|
||||
for(int j = 0; j < 24; j++) {
|
||||
raw_samples_add(samples, true, te);
|
||||
raw_samples_add(samples, false, te);
|
||||
}
|
||||
|
||||
// Sync: 3 alternating 600 us pulse/gap pairs.
|
||||
for(int j = 0; j < 3; j++) {
|
||||
raw_samples_add(samples, true, te * 2);
|
||||
raw_samples_add(samples, false, te * 2);
|
||||
}
|
||||
|
||||
// Sync: plus 2 alternating 300 us pluse/gap pairs.
|
||||
for(int j = 0; j < 2; j++) {
|
||||
raw_samples_add(samples, true, te);
|
||||
raw_samples_add(samples, false, te);
|
||||
}
|
||||
|
||||
// Data: build the array.
|
||||
uint32_t datalen = 1 + fs->fields[0]->len + // Userlen + Username
|
||||
fs->fields[1]->len + 3; // Message + FF + 00 + CRC
|
||||
uint8_t *data = malloc(datalen), *p = data;
|
||||
*p++ = fs->fields[0]->len;
|
||||
memcpy(p, fs->fields[0]->str, fs->fields[0]->len);
|
||||
p += fs->fields[0]->len;
|
||||
memcpy(p, fs->fields[1]->str, fs->fields[1]->len);
|
||||
p += fs->fields[1]->len;
|
||||
*p++ = 0xff;
|
||||
*p++ = 0xaa;
|
||||
*p = sum_bytes(data, datalen - 1, 0);
|
||||
|
||||
// Emit bits
|
||||
for(uint32_t j = 0; j < datalen * 8; j++) {
|
||||
if(bitmap_get(data, datalen, j)) {
|
||||
raw_samples_add(samples, true, te * 2);
|
||||
raw_samples_add(samples, false, te);
|
||||
} else {
|
||||
raw_samples_add(samples, true, te);
|
||||
raw_samples_add(samples, false, te * 2);
|
||||
}
|
||||
}
|
||||
free(data);
|
||||
}
|
||||
|
||||
ProtoViewDecoder ProtoViewChatDecoder = {
|
||||
.name = "ProtoView chat",
|
||||
.decode = decode,
|
||||
.get_fields = get_fields,
|
||||
.build_message = build_message};
|
||||
@@ -1,4 +1,7 @@
|
||||
/* Citroen TPMS. Usually 443.92 Mhz FSK.
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* Citroen TPMS. Usually 443.92 Mhz FSK.
|
||||
*
|
||||
* Preamble of ~14 high/low 52 us pulses
|
||||
* Sync of high 100us pulse then 50us low
|
||||
@@ -43,33 +46,13 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
int temp = raw[7] - 50;
|
||||
int battery = raw[8]; /* This may be the battery. It's not clear. */
|
||||
|
||||
snprintf(info->name, sizeof(info->name), "%s", "Citroen TPMS");
|
||||
snprintf(
|
||||
info->raw,
|
||||
sizeof(info->raw),
|
||||
"%02X%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],
|
||||
raw[9]);
|
||||
snprintf(
|
||||
info->info1,
|
||||
sizeof(info->info1),
|
||||
"Tire ID %02X%02X%02X%02X",
|
||||
raw[1],
|
||||
raw[2],
|
||||
raw[3],
|
||||
raw[4]);
|
||||
snprintf(info->info2, sizeof(info->info2), "Pressure %.2f kpa", (double)kpa);
|
||||
snprintf(info->info3, sizeof(info->info3), "Temperature %d C", temp);
|
||||
snprintf(info->info4, sizeof(info->info4), "Repeat %d, Bat %d", repeat, battery);
|
||||
fieldset_add_bytes(info->fieldset, "Tire ID", raw + 1, 4 * 2);
|
||||
fieldset_add_float(info->fieldset, "Pressure kpa", kpa, 2);
|
||||
fieldset_add_int(info->fieldset, "Temperature C", temp, 8);
|
||||
fieldset_add_uint(info->fieldset, "Repeat", repeat, 4);
|
||||
fieldset_add_uint(info->fieldset, "Battery", battery, 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoViewDecoder CitroenTPMSDecoder = {"Citroen TPMS", decode};
|
||||
ProtoViewDecoder CitroenTPMSDecoder =
|
||||
{.name = "Citroen TPMS", .decode = decode, .get_fields = NULL, .build_message = NULL};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* Ford tires TPMS. Usually 443.92 Mhz FSK (in Europe).
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* Ford tires TPMS. Usually 443.92 Mhz FSK (in Europe).
|
||||
*
|
||||
* 52 us short pules
|
||||
* Preamble: 0101010101010101010101010101
|
||||
@@ -46,34 +49,13 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
int flags = raw[5] & 0x7f;
|
||||
int car_moving = (raw[6] & 0x44) == 0x44;
|
||||
|
||||
snprintf(info->name, sizeof(info->name), "%s", "Ford TPMS");
|
||||
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),
|
||||
"Tire ID %02X%02X%02X%02X",
|
||||
raw[0],
|
||||
raw[1],
|
||||
raw[2],
|
||||
raw[3]);
|
||||
snprintf(info->info2, sizeof(info->info2), "Pressure %.2f psi", (double)psi);
|
||||
if(temp)
|
||||
snprintf(info->info3, sizeof(info->info3), "Temperature %d C", temp);
|
||||
else
|
||||
snprintf(info->info3, sizeof(info->info3), "Flags %d", flags);
|
||||
snprintf(info->info4, sizeof(info->info4), "Moving %s", car_moving ? "yes" : "no");
|
||||
fieldset_add_bytes(info->fieldset, "Tire ID", raw, 4 * 2);
|
||||
fieldset_add_float(info->fieldset, "Pressure psi", psi, 2);
|
||||
fieldset_add_int(info->fieldset, "Temperature C", temp, 8);
|
||||
fieldset_add_hex(info->fieldset, "Flags", flags, 7);
|
||||
fieldset_add_uint(info->fieldset, "Moving", car_moving, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoViewDecoder FordTPMSDecoder = {"Ford TPMS", decode};
|
||||
ProtoViewDecoder FordTPMSDecoder =
|
||||
{.name = "Ford TPMS", .decode = decode, .get_fields = NULL, .build_message = NULL};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* Renault tires TPMS. Usually 443.92 Mhz FSK.
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* Renault tires TPMS. Usually 443.92 Mhz FSK.
|
||||
*
|
||||
* Preamble + sync + Manchester bits. ~48us short pulse.
|
||||
* 9 Bytes in total not counting the preamble. */
|
||||
@@ -48,27 +51,69 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
|
||||
info->pulses_count = (off + 8 * 9 * 2) - info->start_off;
|
||||
|
||||
uint8_t flags = raw[0] >> 2;
|
||||
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), "Tire ID %02X%02X%02X", raw[3], raw[4], raw[5]);
|
||||
snprintf(info->info2, sizeof(info->info2), "Pressure %.2f kpa", (double)kpa);
|
||||
snprintf(info->info3, sizeof(info->info3), "Temperature %d C", temp);
|
||||
fieldset_add_bytes(info->fieldset, "Tire ID", raw + 3, 3 * 2);
|
||||
fieldset_add_float(info->fieldset, "Pressure kpa", kpa, 2);
|
||||
fieldset_add_int(info->fieldset, "Temperature C", temp, 8);
|
||||
fieldset_add_hex(info->fieldset, "Flags", flags, 6);
|
||||
fieldset_add_bytes(info->fieldset, "Unknown1", raw + 6, 2);
|
||||
fieldset_add_bytes(info->fieldset, "Unknown2", raw + 7, 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoViewDecoder RenaultTPMSDecoder = {"Renault TPMS", decode};
|
||||
/* Give fields and defaults for the signal creator. */
|
||||
static void get_fields(ProtoViewFieldSet* fieldset) {
|
||||
uint8_t default_id[3] = {0xAB, 0xCD, 0xEF};
|
||||
fieldset_add_bytes(fieldset, "Tire ID", default_id, 3 * 2);
|
||||
fieldset_add_float(fieldset, "Pressure kpa", 123, 2);
|
||||
fieldset_add_int(fieldset, "Temperature C", 20, 8);
|
||||
// We don't know what flags are, but 1B is a common value.
|
||||
fieldset_add_hex(fieldset, "Flags", 0x1b, 6);
|
||||
fieldset_add_bytes(fieldset, "Unknown1", (uint8_t*)"\xff", 2);
|
||||
fieldset_add_bytes(fieldset, "Unknown2", (uint8_t*)"\xff", 2);
|
||||
}
|
||||
|
||||
/* Create a Renault TPMS signal, according to the fields provided. */
|
||||
static void build_message(RawSamplesBuffer* samples, ProtoViewFieldSet* fieldset) {
|
||||
uint32_t te = 50; // Short pulse duration in microseconds.
|
||||
|
||||
// Preamble + sync
|
||||
const char* psync = "01010101010101010101010101010110";
|
||||
const char* p = psync;
|
||||
while(*p) {
|
||||
raw_samples_add_or_update(samples, *p == '1', te);
|
||||
p++;
|
||||
}
|
||||
|
||||
// Data, 9 bytes
|
||||
uint8_t data[9] = {0};
|
||||
unsigned int raw_pressure = fieldset->fields[1]->fvalue * 4 / 3;
|
||||
data[0] = fieldset->fields[3]->uvalue << 2; // Flags
|
||||
data[0] |= (raw_pressure >> 8) & 3; // Pressure kpa high 2 bits
|
||||
data[1] = raw_pressure & 0xff; // Pressure kpa low 8 bits
|
||||
data[2] = fieldset->fields[2]->value + 30; // Temperature C
|
||||
memcpy(data + 3, fieldset->fields[0]->bytes, 6); // ID, 24 bits.
|
||||
data[6] = fieldset->fields[4]->bytes[0]; // Unknown 1
|
||||
data[7] = fieldset->fields[5]->bytes[0]; // Unknown 2
|
||||
data[8] = crc8(data, 8, 0, 7);
|
||||
|
||||
// Generate Manchester code for each bit
|
||||
for(uint32_t j = 0; j < 9 * 8; j++) {
|
||||
if(bitmap_get(data, sizeof(data), j)) {
|
||||
raw_samples_add_or_update(samples, true, te);
|
||||
raw_samples_add_or_update(samples, false, te);
|
||||
} else {
|
||||
raw_samples_add_or_update(samples, false, te);
|
||||
raw_samples_add_or_update(samples, true, te);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProtoViewDecoder RenaultTPMSDecoder = {
|
||||
.name = "Renault TPMS",
|
||||
.decode = decode,
|
||||
.get_fields = get_fields,
|
||||
.build_message = build_message};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* Schrader TPMS. Usually 443.92 Mhz OOK, 120us pulse len.
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* Schrader TPMS. Usually 443.92 Mhz OOK, 120us pulse len.
|
||||
*
|
||||
* 500us high pulse + Preamble + Manchester coded bits where:
|
||||
* 1 = 10
|
||||
@@ -34,6 +37,7 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
0011 = 0x3. */
|
||||
|
||||
uint8_t raw[8];
|
||||
uint8_t id[4];
|
||||
uint32_t decoded = convert_from_line_code(
|
||||
raw, sizeof(raw), bits, numbytes, off, "01", "10"); /* Manchester code. */
|
||||
FURI_LOG_E(TAG, "Schrader TPMS decoded bits: %lu", decoded);
|
||||
@@ -51,31 +55,16 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
|
||||
float kpa = (float)raw[5] * 2.5;
|
||||
int temp = raw[6] - 50;
|
||||
id[0] = raw[1] & 7;
|
||||
id[1] = raw[2];
|
||||
id[2] = raw[3];
|
||||
id[3] = raw[4];
|
||||
|
||||
snprintf(info->name, sizeof(info->name), "%s", "Schrader TPMS");
|
||||
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),
|
||||
"Tire ID %01X%02X%02X%02X",
|
||||
raw[1] & 7,
|
||||
raw[2],
|
||||
raw[3],
|
||||
raw[4]); /* Only 28 bits of ID, not 32. */
|
||||
snprintf(info->info2, sizeof(info->info2), "Pressure %.2f kpa", (double)kpa);
|
||||
snprintf(info->info3, sizeof(info->info3), "Temperature %d C", temp);
|
||||
fieldset_add_bytes(info->fieldset, "Tire ID", id, 4 * 2);
|
||||
fieldset_add_float(info->fieldset, "Pressure kpa", kpa, 2);
|
||||
fieldset_add_int(info->fieldset, "Temperature C", temp, 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoViewDecoder SchraderTPMSDecoder = {"Schrader TPMS", decode};
|
||||
ProtoViewDecoder SchraderTPMSDecoder =
|
||||
{.name = "Schrader TPMS", .decode = decode, .get_fields = NULL, .build_message = NULL};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* Schrader variant EG53MA4 TPMS.
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* Schrader variant EG53MA4 TPMS.
|
||||
* Usually 443.92 Mhz OOK, 100us pulse len.
|
||||
*
|
||||
* Preamble: alternating pulse/gap, 100us.
|
||||
@@ -49,31 +52,11 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
int temp_f = raw[8];
|
||||
int temp_c = (temp_f - 32) * 5 / 9; /* Convert Fahrenheit to Celsius. */
|
||||
|
||||
snprintf(info->name, sizeof(info->name), "%s", "Schrader EG53MA4 TPMS");
|
||||
snprintf(
|
||||
info->raw,
|
||||
sizeof(info->raw),
|
||||
"%02X%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],
|
||||
raw[9]);
|
||||
snprintf(
|
||||
info->info1,
|
||||
sizeof(info->info1),
|
||||
"Tire ID %02X%02X%02X",
|
||||
raw[4],
|
||||
raw[5],
|
||||
raw[6]); /* Only 28 bits of ID, not 32. */
|
||||
snprintf(info->info2, sizeof(info->info2), "Pressure %.2f kpa", (double)kpa);
|
||||
snprintf(info->info3, sizeof(info->info3), "Temperature %d C", temp_c);
|
||||
fieldset_add_bytes(info->fieldset, "Tire ID", raw + 4, 3 * 2);
|
||||
fieldset_add_float(info->fieldset, "Pressure kpa", kpa, 2);
|
||||
fieldset_add_int(info->fieldset, "Temperature C", temp_c, 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoViewDecoder SchraderEG53MA4TPMSDecoder = {"Schrader EG53MA4 TPMS", decode};
|
||||
ProtoViewDecoder SchraderEG53MA4TPMSDecoder =
|
||||
{.name = "Schrader EG53MA4 TPMS", .decode = decode, .get_fields = NULL, .build_message = NULL};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* Toyota tires TPMS. Usually 443.92 Mhz FSK (In Europe).
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* Toyota tires TPMS. Usually 443.92 Mhz FSK (In Europe).
|
||||
*
|
||||
* Preamble + sync + 64 bits of data. ~48us short pulse length.
|
||||
*
|
||||
@@ -65,34 +68,14 @@ static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoView
|
||||
|
||||
info->pulses_count = (off + 8 * 9 * 2) - info->start_off;
|
||||
|
||||
float kpa = (float)((raw[4] & 0x7f) << 1 | raw[5] >> 7) * 0.25 - 7;
|
||||
float psi = (float)((raw[4] & 0x7f) << 1 | raw[5] >> 7) * 0.25 - 7;
|
||||
int temp = ((raw[5] & 0x7f) << 1 | raw[6] >> 7) - 40;
|
||||
|
||||
snprintf(info->name, sizeof(info->name), "%s", "Toyota 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),
|
||||
"Tire ID %02X%02X%02X%02X",
|
||||
raw[0],
|
||||
raw[1],
|
||||
raw[2],
|
||||
raw[3]);
|
||||
snprintf(info->info2, sizeof(info->info2), "Pressure %.2f psi", (double)kpa);
|
||||
snprintf(info->info3, sizeof(info->info3), "Temperature %d C", temp);
|
||||
fieldset_add_bytes(info->fieldset, "Tire ID", raw, 4 * 2);
|
||||
fieldset_add_float(info->fieldset, "Pressure psi", psi, 2);
|
||||
fieldset_add_int(info->fieldset, "Temperature C", temp, 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoViewDecoder ToyotaTPMSDecoder = {"Toyota TPMS", decode};
|
||||
ProtoViewDecoder ToyotaTPMSDecoder =
|
||||
{.name = "Toyota TPMS", .decode = decode, .get_fields = NULL, .build_message = NULL};
|
||||
|
||||
326
applications/plugins/protoview/protocols/unknown.c
Normal file
326
applications/plugins/protoview/protocols/unknown.c
Normal file
@@ -0,0 +1,326 @@
|
||||
#include "../app.h"
|
||||
|
||||
/* Copyright (C) 2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license.
|
||||
*
|
||||
* ----------------------------------------------------------------------------
|
||||
* The "unknown" decoder fires as the last one, once we are sure no other
|
||||
* decoder was able to identify the signal. The goal is to detect the
|
||||
* preamble and line code used in the received signal, then turn the
|
||||
* decoded bits into bytes.
|
||||
*
|
||||
* The techniques used for the detection are described in the comments
|
||||
* below.
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/* Scan the signal bitmap looking for a PWM modulation. In this case
|
||||
* for PWM we are referring to two exact patterns of high and low
|
||||
* signal (each bit in the bitmap is worth the smallest gap/pulse duration
|
||||
* we detected) that repeat each other in a given segment of the message.
|
||||
*
|
||||
* This modulation is quite common, for instance sometimes zero and
|
||||
* one are rappresented by a 700us pulse followed by 350 gap,
|
||||
* and 350us pulse followed by a 700us gap. So the signal bitmap received
|
||||
* by the decoder would contain 110 and 100 symbols.
|
||||
*
|
||||
* The way this function work is commented inline.
|
||||
*
|
||||
* The function returns the number of consecutive symbols found, having
|
||||
* a symbol length of 'symlen' (3 in the above example), and stores
|
||||
* in *s1i the offset of the first symbol found, and in *s2i the offset
|
||||
* of the second symbol. The function can't tell which is one and which
|
||||
* zero. */
|
||||
static uint32_t find_pwm(
|
||||
uint8_t* bits,
|
||||
uint32_t numbytes,
|
||||
uint32_t numbits,
|
||||
uint32_t symlen,
|
||||
uint32_t* s1i,
|
||||
uint32_t* s2i) {
|
||||
uint32_t best_count = 0; /* Max number of symbols found in this try. */
|
||||
uint32_t best_idx1 = 0; /* First symbol offset of longest sequence found.
|
||||
* This is also the start sequence offset. */
|
||||
uint32_t best_idx2 = 0; /* Second symbol offset. */
|
||||
|
||||
/* Try all the possible symbol offsets that are less of our
|
||||
* symbol len. This is likely not really useful but we take
|
||||
* a conservative approach. Because if have have, for instance,
|
||||
* repeating symbols "100" and "110", they will form a sequence
|
||||
* that is choerent at different offsets, but out-of-sync.
|
||||
*
|
||||
* Anyway at the end of the function we try to fix the sync. */
|
||||
for(uint32_t off = 0; off < symlen; off++) {
|
||||
uint32_t c = 0; // Number of contiguous symbols found.
|
||||
uint32_t c1 = 0, c2 = 0; // Occurrences of first/second symbol.
|
||||
*s1i = off; // Assume we start at one symbol boundaty.
|
||||
*s2i = UINT32_MAX; // Second symbol first index still unknown.
|
||||
uint32_t next = off;
|
||||
|
||||
/* We scan the whole bitmap in one pass, resetting the state
|
||||
* each time we find a pattern that is not one of the two
|
||||
* symbols we found so far. */
|
||||
while(next < numbits - symlen) {
|
||||
bool match1 = bitmap_match_bitmap(bits, numbytes, next, bits, numbytes, *s1i, symlen);
|
||||
if(!match1 && *s2i == UINT32_MAX) {
|
||||
/* It's not the first sybol. We don't know how the
|
||||
* second look like. Assume we found an occurrence of
|
||||
* the second symbol. */
|
||||
*s2i = next;
|
||||
}
|
||||
|
||||
bool match2 = bitmap_match_bitmap(bits, numbytes, next, bits, numbytes, *s2i, symlen);
|
||||
|
||||
/* One or the other should match. */
|
||||
if(match1 || match2) {
|
||||
c++;
|
||||
if(match1) c1++;
|
||||
if(match2) c2++;
|
||||
if(c > best_count && c1 >= best_count / 5 && // Require enough presence of both
|
||||
c2 >= best_count / 5) // zero and one.
|
||||
{
|
||||
best_count = c;
|
||||
best_idx1 = *s1i;
|
||||
best_idx2 = *s2i;
|
||||
}
|
||||
next += symlen;
|
||||
} else {
|
||||
/* No match. Continue resetting the signal info. */
|
||||
c = 0; // Start again to count contiguous symbols.
|
||||
c1 = 0;
|
||||
c2 = 0;
|
||||
*s1i = next; // First symbol always at start.
|
||||
*s2i = UINT32_MAX; // Second symbol unknown.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* We don't know if we are really synchronized with the bits at this point.
|
||||
* For example if zero bit is 100 and one bit is 110 in a specific
|
||||
* line code, our detector could randomly believe it's 001 and 101.
|
||||
* However PWD line codes normally start with a pulse in both symbols.
|
||||
* If that is the case, let's align. */
|
||||
uint32_t shift;
|
||||
for(shift = 0; shift < symlen; shift++) {
|
||||
if(bitmap_get(bits, numbytes, best_idx1 + shift) &&
|
||||
bitmap_get(bits, numbytes, best_idx2 + shift))
|
||||
break;
|
||||
}
|
||||
if(shift != symlen) {
|
||||
best_idx1 += shift;
|
||||
best_idx2 += shift;
|
||||
}
|
||||
|
||||
*s1i = best_idx1;
|
||||
*s2i = best_idx2;
|
||||
return best_count;
|
||||
}
|
||||
|
||||
/* Find the longest sequence that looks like Manchester coding.
|
||||
*
|
||||
* Manchester coding requires each pairs of bits to be either
|
||||
* 01 or 10. We'll have to try odd and even offsets to be
|
||||
* sure to find it.
|
||||
*
|
||||
* Note that this will also detect differential Manchester, but
|
||||
* will report it as Manchester. I can't think of any way to
|
||||
* distinguish between the two line codes, because shifting them
|
||||
* one symbol will make one to look like the other.
|
||||
*
|
||||
* Only option could be to decode the message with both line
|
||||
* codes and use statistical properties (common byte values)
|
||||
* to determine what's more likely, but this looks very fragile.
|
||||
*
|
||||
* Fortunately differential Manchester is more rarely used,
|
||||
* so we can assume Manchester most of the times. Yet we are left
|
||||
* with the indetermination about zero being pulse-gap or gap-pulse
|
||||
* or the other way around.
|
||||
*
|
||||
* If the 'only_raising' parameter is true, the function detects
|
||||
* only sequences going from gap to pulse: this is useful in order
|
||||
* to locate preambles of alternating gaps and pulses. */
|
||||
static uint32_t find_alternating_bits(
|
||||
uint8_t* bits,
|
||||
uint32_t numbytes,
|
||||
uint32_t numbits,
|
||||
uint32_t* start,
|
||||
bool only_raising) {
|
||||
uint32_t best_count = 0; // Max number of symbols found
|
||||
uint32_t best_off = 0; // Max symbols start offset.
|
||||
for(int odd = 0; odd < 2; odd++) {
|
||||
uint32_t count = 0; // Symbols found so far
|
||||
uint32_t start_off = odd;
|
||||
uint32_t j = odd;
|
||||
while(j < numbits - 1) {
|
||||
bool bit1 = bitmap_get(bits, numbytes, j);
|
||||
bool bit2 = bitmap_get(bits, numbytes, j + 1);
|
||||
if((!only_raising && bit1 != bit2) || (only_raising && !bit1 && bit2)) {
|
||||
count++;
|
||||
if(count > best_count) {
|
||||
best_count = count;
|
||||
best_off = start_off;
|
||||
}
|
||||
} else {
|
||||
/* End of sequence. Continue with the next
|
||||
* part of the signal. */
|
||||
count = 0;
|
||||
start_off = j + 2;
|
||||
}
|
||||
j += 2;
|
||||
}
|
||||
}
|
||||
*start = best_off;
|
||||
return best_count;
|
||||
}
|
||||
|
||||
/* Wrapper to find Manchester code. */
|
||||
static uint32_t
|
||||
find_manchester(uint8_t* bits, uint32_t numbytes, uint32_t numbits, uint32_t* start) {
|
||||
return find_alternating_bits(bits, numbytes, numbits, start, false);
|
||||
}
|
||||
|
||||
/* Wrapper to find preamble sections. */
|
||||
static uint32_t
|
||||
find_preamble(uint8_t* bits, uint32_t numbytes, uint32_t numbits, uint32_t* start) {
|
||||
return find_alternating_bits(bits, numbytes, numbits, start, true);
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
LineCodeNone,
|
||||
LineCodeManchester,
|
||||
LineCodePWM3,
|
||||
LineCodePWM4,
|
||||
} LineCodeGuess;
|
||||
|
||||
static char* get_linecode_name(LineCodeGuess lc) {
|
||||
switch(lc) {
|
||||
case LineCodeNone:
|
||||
return "none";
|
||||
case LineCodeManchester:
|
||||
return "Manchester";
|
||||
case LineCodePWM3:
|
||||
return "PWM3";
|
||||
case LineCodePWM4:
|
||||
return "PWM4";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
|
||||
/* No decoder was able to detect this message. Let's try if we can
|
||||
* find some structure. To start, we'll see if it looks like is
|
||||
* manchester coded, or PWM with symbol len of 3 or 4. */
|
||||
|
||||
/* For PWM, start1 and start2 are the offsets at which the two
|
||||
* sequences composing the message appear the first time.
|
||||
* So start1 is also the message start offset. Start2 is not used
|
||||
* for Manchester, that does not have two separated symbols like
|
||||
* PWM. */
|
||||
uint32_t start1 = 0, start2 = 0;
|
||||
uint32_t msgbits; // Number of message bits in the bitmap, so
|
||||
// this will be the number of symbols, not actual
|
||||
// bits after the message is decoded.
|
||||
uint32_t tmp1, tmp2; // Temp vars to store the start.
|
||||
uint32_t minbits = 16; // Less than that gets undetected.
|
||||
uint32_t pwm_len; // Bits per symbol, in the case of PWM.
|
||||
LineCodeGuess linecode = LineCodeNone;
|
||||
|
||||
// Try PWM3
|
||||
uint32_t pwm3_bits = find_pwm(bits, numbytes, numbits, 3, &tmp1, &tmp2);
|
||||
if(pwm3_bits >= minbits) {
|
||||
linecode = LineCodePWM3;
|
||||
start1 = tmp1;
|
||||
start2 = tmp2;
|
||||
pwm_len = 3;
|
||||
msgbits = pwm3_bits * pwm_len;
|
||||
}
|
||||
|
||||
// Try PWM4
|
||||
uint32_t pwm4_bits = find_pwm(bits, numbytes, numbits, 4, &tmp1, &tmp2);
|
||||
if(pwm4_bits >= minbits && pwm4_bits > pwm3_bits) {
|
||||
linecode = LineCodePWM4;
|
||||
start1 = tmp1;
|
||||
start2 = tmp2;
|
||||
pwm_len = 4;
|
||||
msgbits = pwm3_bits * pwm_len;
|
||||
}
|
||||
|
||||
// Try Manchester
|
||||
uint32_t manchester_bits = find_manchester(bits, numbytes, numbits, &tmp1);
|
||||
if(manchester_bits > minbits && manchester_bits > pwm3_bits && manchester_bits > pwm4_bits) {
|
||||
linecode = LineCodeManchester;
|
||||
start1 = tmp1;
|
||||
msgbits = manchester_bits * 2;
|
||||
FURI_LOG_E(TAG, "MANCHESTER START: %lu", tmp1);
|
||||
}
|
||||
|
||||
if(linecode == LineCodeNone) return false;
|
||||
|
||||
/* Often there is a preamble before the signal. We'll try to find
|
||||
* it, and if it is not too far away from our signal, we'll claim
|
||||
* our signal starts at the preamble. */
|
||||
uint32_t preamble_len = find_preamble(bits, numbytes, numbits, &tmp1);
|
||||
uint32_t min_preamble_len = 10;
|
||||
uint32_t max_preamble_distance = 32;
|
||||
uint32_t preamble_start = 0;
|
||||
bool preamble_found = false;
|
||||
|
||||
/* Note that because of the following checks, if the Manchester detector
|
||||
* detected the preamble bits as data, we are ok with that, since it
|
||||
* means that the synchronization is not designed to "break" the bits
|
||||
* flow. In this case we ignore the preamble and return all as data. */
|
||||
if(preamble_len >= min_preamble_len && // Not too short.
|
||||
tmp1 < start1 && // Should be before the data.
|
||||
start1 - tmp1 <= max_preamble_distance) // Not too far.
|
||||
{
|
||||
preamble_start = tmp1;
|
||||
preamble_found = true;
|
||||
}
|
||||
|
||||
info->start_off = preamble_found ? preamble_start : start1;
|
||||
info->pulses_count = (start1 + msgbits) - info->start_off;
|
||||
info->pulses_count += 20; /* Add a few more, so that if the user resends
|
||||
* the message, it is more likely we will
|
||||
* transfer all that is needed, like a message
|
||||
* terminator (that we don't detect). */
|
||||
|
||||
if(preamble_found) FURI_LOG_E(TAG, "PREAMBLE AT: %lu", preamble_start);
|
||||
FURI_LOG_E(TAG, "START: %lu", info->start_off);
|
||||
FURI_LOG_E(TAG, "MSGBITS: %lu", msgbits);
|
||||
FURI_LOG_E(TAG, "DATASTART: %lu", start1);
|
||||
FURI_LOG_E(TAG, "PULSES: %lu", info->pulses_count);
|
||||
|
||||
/* We think there is a message and we know where it starts and the
|
||||
* line code used. We can turn it into bits and bytes. */
|
||||
uint32_t decoded;
|
||||
uint8_t data[32];
|
||||
uint32_t datalen;
|
||||
|
||||
char symbol1[5], symbol2[5];
|
||||
if(linecode == LineCodePWM3 || linecode == LineCodePWM4) {
|
||||
bitmap_to_string(symbol1, bits, numbytes, start1, pwm_len);
|
||||
bitmap_to_string(symbol2, bits, numbytes, start2, pwm_len);
|
||||
} else if(linecode == LineCodeManchester) {
|
||||
memcpy(symbol1, "01", 3);
|
||||
memcpy(symbol2, "10", 3);
|
||||
}
|
||||
|
||||
decoded = convert_from_line_code(data, sizeof(data), bits, numbytes, start1, symbol1, symbol2);
|
||||
datalen = (decoded + 7) / 8;
|
||||
|
||||
char* linecode_name = get_linecode_name(linecode);
|
||||
fieldset_add_str(info->fieldset, "line code", linecode_name, strlen(linecode_name));
|
||||
fieldset_add_uint(info->fieldset, "data bits", decoded, 8);
|
||||
if(preamble_found) fieldset_add_uint(info->fieldset, "preamble len", preamble_len, 8);
|
||||
fieldset_add_str(info->fieldset, "first symbol", symbol1, strlen(symbol1));
|
||||
fieldset_add_str(info->fieldset, "second symbol", symbol2, strlen(symbol2));
|
||||
for(uint32_t j = 0; j < datalen; j++) {
|
||||
char label[16];
|
||||
snprintf(label, sizeof(label), "data[%lu]", j);
|
||||
fieldset_add_bytes(info->fieldset, label, data + j, 2);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoViewDecoder UnknownDecoder =
|
||||
{.name = "Unknown", .decode = decode, .get_fields = NULL, .build_message = NULL};
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <furi/core/string.h>
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include "app_buffer.h"
|
||||
#include "raw_samples.h"
|
||||
|
||||
/* Allocate and initialize a samples buffer. */
|
||||
RawSamplesBuffer* raw_samples_alloc(void) {
|
||||
@@ -48,6 +48,29 @@ void raw_samples_add(RawSamplesBuffer* s, bool level, uint32_t dur) {
|
||||
furi_mutex_release(s->mutex);
|
||||
}
|
||||
|
||||
/* This is like raw_samples_add(), however in case a sample of the
|
||||
* same level of the previous one is added, the duration of the last
|
||||
* sample is updated instead. Needed mainly for the decoders build_message()
|
||||
* methods: it is simpler to write an encoder of a signal like that,
|
||||
* just creating messages piece by piece.
|
||||
*
|
||||
* This function is a bit slower so the internal data sampling should
|
||||
* be performed with raw_samples_add(). */
|
||||
void raw_samples_add_or_update(RawSamplesBuffer* s, bool level, uint32_t dur) {
|
||||
furi_mutex_acquire(s->mutex, FuriWaitForever);
|
||||
uint32_t previdx = (s->idx - 1) % RAW_SAMPLES_NUM;
|
||||
if(s->samples[previdx].level == level && s->samples[previdx].dur != 0) {
|
||||
/* Update the last sample: it has the same level. */
|
||||
s->samples[previdx].dur += dur;
|
||||
} else {
|
||||
/* Add a new sample. */
|
||||
s->samples[s->idx].level = level;
|
||||
s->samples[s->idx].dur = 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) {
|
||||
@@ -27,6 +27,7 @@ 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_add_or_update(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);
|
||||
@@ -5,6 +5,43 @@
|
||||
|
||||
bool decode_signal(RawSamplesBuffer* s, uint64_t len, ProtoViewMsgInfo* info);
|
||||
|
||||
/* =============================================================================
|
||||
* Protocols table.
|
||||
*
|
||||
* Supported protocols go here, with the relevant implementation inside
|
||||
* protocols/<name>.c
|
||||
* ===========================================================================*/
|
||||
|
||||
extern ProtoViewDecoder Oregon2Decoder;
|
||||
extern ProtoViewDecoder B4B1Decoder;
|
||||
extern ProtoViewDecoder RenaultTPMSDecoder;
|
||||
extern ProtoViewDecoder ToyotaTPMSDecoder;
|
||||
extern ProtoViewDecoder SchraderTPMSDecoder;
|
||||
extern ProtoViewDecoder SchraderEG53MA4TPMSDecoder;
|
||||
extern ProtoViewDecoder CitroenTPMSDecoder;
|
||||
extern ProtoViewDecoder FordTPMSDecoder;
|
||||
extern ProtoViewDecoder KeeloqDecoder;
|
||||
extern ProtoViewDecoder ProtoViewChatDecoder;
|
||||
extern ProtoViewDecoder UnknownDecoder;
|
||||
|
||||
ProtoViewDecoder* Decoders[] = {
|
||||
&Oregon2Decoder, /* Oregon sensors v2.1 protocol. */
|
||||
&B4B1Decoder, /* PT, SC, ... 24 bits remotes. */
|
||||
&RenaultTPMSDecoder, /* Renault TPMS. */
|
||||
&ToyotaTPMSDecoder, /* Toyota TPMS. */
|
||||
&SchraderTPMSDecoder, /* Schrader TPMS. */
|
||||
&SchraderEG53MA4TPMSDecoder, /* Schrader EG53MA4 TPMS. */
|
||||
&CitroenTPMSDecoder, /* Citroen TPMS. */
|
||||
&FordTPMSDecoder, /* Ford TPMS. */
|
||||
&KeeloqDecoder, /* Keeloq remote. */
|
||||
&ProtoViewChatDecoder, /* Protoview simple text messages. */
|
||||
|
||||
/* Warning: the following decoder must stay at the end of the
|
||||
* list. Otherwise would detect most signals and prevent the actaul
|
||||
* decoders from handling them. */
|
||||
&UnknownDecoder, /* General protocol detector. */
|
||||
NULL};
|
||||
|
||||
/* =============================================================================
|
||||
* Raw signal detection
|
||||
* ===========================================================================*/
|
||||
@@ -39,23 +76,28 @@ void reset_current_signal(ProtoViewApp* app) {
|
||||
* 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) {
|
||||
uint32_t search_coherent_signal(RawSamplesBuffer* s, uint32_t idx, uint32_t min_duration) {
|
||||
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. */
|
||||
|
||||
// Set a min/max duration limit for samples to be considered part of a
|
||||
// coherent signal. The maximum length is fixed while the minimum
|
||||
// is passed as argument, as depends on the data rate and in general
|
||||
// on the signal to analyze.
|
||||
uint32_t max_duration = 4000;
|
||||
|
||||
uint32_t len = 0; /* Observed len of coherent samples. */
|
||||
s->short_pulse_dur = 0;
|
||||
for(uint32_t j = idx; j < idx + 500; j++) {
|
||||
for(uint32_t j = idx; j < idx + s->total; j++) {
|
||||
bool level;
|
||||
uint32_t dur;
|
||||
raw_samples_get(s, j, &level, &dur);
|
||||
|
||||
if(dur < minlen || dur > maxlen) break; /* return. */
|
||||
if(dur < min_duration || dur > max_duration) break; /* return. */
|
||||
|
||||
/* Let's see if it matches a class we already have or if we
|
||||
* can populate a new (yet empty) class. */
|
||||
@@ -138,15 +180,15 @@ void notify_signal_detected(ProtoViewApp* app, bool decoded) {
|
||||
notification_message(app->notification, &unknown_seq);
|
||||
}
|
||||
|
||||
/* Search the buffer with the stored signal (last N samples received)
|
||||
/* Search the source 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
|
||||
void scan_for_signal(ProtoViewApp* app, RawSamplesBuffer* source, uint32_t min_duration) {
|
||||
/* We need to work on a copy: the source buffer may be populated
|
||||
* by the background thread receiving data. */
|
||||
RawSamplesBuffer* copy = raw_samples_alloc();
|
||||
raw_samples_copy(copy, RawSamples);
|
||||
raw_samples_copy(copy, source);
|
||||
|
||||
/* Try to seek on data that looks to have a regular high low high low
|
||||
* pattern. */
|
||||
@@ -157,7 +199,7 @@ void scan_for_signal(ProtoViewApp* app) {
|
||||
uint32_t i = 0;
|
||||
|
||||
while(i < copy->total - 1) {
|
||||
uint32_t thislen = search_coherent_signal(copy, i);
|
||||
uint32_t thislen = search_coherent_signal(copy, i, min_duration);
|
||||
|
||||
/* For messages that are long enough, attempt decoding. */
|
||||
if(thislen > minlen) {
|
||||
@@ -179,8 +221,11 @@ void scan_for_signal(ProtoViewApp* app) {
|
||||
/* Accept this signal as the new signal if either it's longer
|
||||
* than the previous undecoded one, or the previous one was
|
||||
* unknown and this is decoded. */
|
||||
if((thislen > app->signal_bestlen && app->signal_decoded == false) ||
|
||||
(app->signal_decoded == false && decoded)) {
|
||||
bool oldsignal_not_decoded = app->signal_decoded == false ||
|
||||
app->msg_info->decoder == &UnknownDecoder;
|
||||
|
||||
if(oldsignal_not_decoded &&
|
||||
(thislen > app->signal_bestlen || (decoded && info->decoder != &UnknownDecoder))) {
|
||||
free_msg_info(app->msg_info);
|
||||
app->msg_info = info;
|
||||
app->signal_bestlen = thislen;
|
||||
@@ -193,13 +238,8 @@ void scan_for_signal(ProtoViewApp* app) {
|
||||
(int)thislen,
|
||||
DetectedSamples->short_pulse_dur);
|
||||
|
||||
/* Adjust raw view scale if the signal has an high
|
||||
* data rate. */
|
||||
if(DetectedSamples->short_pulse_dur < 75)
|
||||
app->us_scale = 10;
|
||||
else if(DetectedSamples->short_pulse_dur < 145)
|
||||
app->us_scale = 30;
|
||||
notify_signal_detected(app, decoded);
|
||||
adjust_raw_view_scale(app, DetectedSamples->short_pulse_dur);
|
||||
if(app->msg_info->decoder != &UnknownDecoder) notify_signal_detected(app, decoded);
|
||||
} else {
|
||||
/* If the structure was not filled, discard it. Otherwise
|
||||
* now the owner is app->msg_info. */
|
||||
@@ -346,7 +386,7 @@ void bitmap_copy(
|
||||
/* We decode bits assuming the first bit we receive is the MSB
|
||||
* (see bitmap_set/get functions). Certain devices send data
|
||||
* encoded in the reverse way. */
|
||||
void bitmap_reverse_bytes(uint8_t* p, uint32_t len) {
|
||||
void bitmap_reverse_bytes_bits(uint8_t* p, uint32_t len) {
|
||||
for(uint32_t j = 0; j < len; j++) {
|
||||
uint32_t b = p[j];
|
||||
/* Step 1: swap the two nibbles: 12345678 -> 56781234 */
|
||||
@@ -392,6 +432,33 @@ uint32_t bitmap_seek_bits(
|
||||
return BITMAP_SEEK_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Compare bitmaps b1 and b2 (possibly overlapping or the same bitmap),
|
||||
* at the specified offsets, for cmplen bits. Returns true if the
|
||||
* exact same bits are found, otherwise false. */
|
||||
bool bitmap_match_bitmap(
|
||||
uint8_t* b1,
|
||||
uint32_t b1len,
|
||||
uint32_t b1off,
|
||||
uint8_t* b2,
|
||||
uint32_t b2len,
|
||||
uint32_t b2off,
|
||||
uint32_t cmplen) {
|
||||
for(uint32_t j = 0; j < cmplen; j++) {
|
||||
bool bit1 = bitmap_get(b1, b1len, b1off + j);
|
||||
bool bit2 = bitmap_get(b2, b2len, b2off + j);
|
||||
if(bit1 != bit2) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Convert 'len' bitmap bits of the bitmap 'bitmap' into a null terminated
|
||||
* string, stored at 'dst', that must have space at least for len+1 bytes.
|
||||
* The bits are extracted from the specified offset. */
|
||||
void bitmap_to_string(char* dst, uint8_t* b, uint32_t blen, uint32_t off, uint32_t len) {
|
||||
for(uint32_t j = 0; j < len; j++) dst[j] = bitmap_get(b, blen, off + j) ? '1' : '0';
|
||||
dst[len] = 0;
|
||||
}
|
||||
|
||||
/* Set the pattern 'pat' into the bitmap 'b' of max length 'blen' bytes,
|
||||
* starting from the specified offset.
|
||||
*
|
||||
@@ -508,7 +575,7 @@ uint32_t convert_from_line_code(
|
||||
}
|
||||
|
||||
/* Convert the differential Manchester code to bits. This is similar to
|
||||
* convert_from_line_code() but specific for Manchester. The user must
|
||||
* convert_from_line_code() but specific for diff-Manchester. The user must
|
||||
* supply the value of the previous symbol before this stream, since
|
||||
* in differential codings the next bits depend on the previous one.
|
||||
*
|
||||
@@ -533,34 +600,10 @@ uint32_t convert_from_diff_manchester(
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/* Supported protocols go here, with the relevant implementation inside
|
||||
* protocols/<name>.c */
|
||||
|
||||
extern ProtoViewDecoder Oregon2Decoder;
|
||||
extern ProtoViewDecoder B4B1Decoder;
|
||||
extern ProtoViewDecoder RenaultTPMSDecoder;
|
||||
extern ProtoViewDecoder ToyotaTPMSDecoder;
|
||||
extern ProtoViewDecoder SchraderTPMSDecoder;
|
||||
extern ProtoViewDecoder SchraderEG53MA4TPMSDecoder;
|
||||
extern ProtoViewDecoder CitroenTPMSDecoder;
|
||||
extern ProtoViewDecoder FordTPMSDecoder;
|
||||
extern ProtoViewDecoder KeeloqDecoder;
|
||||
|
||||
ProtoViewDecoder* Decoders[] = {
|
||||
&Oregon2Decoder, /* Oregon sensors v2.1 protocol. */
|
||||
&B4B1Decoder, /* PT, SC, ... 24 bits remotes. */
|
||||
&RenaultTPMSDecoder, /* Renault TPMS. */
|
||||
&ToyotaTPMSDecoder, /* Toyota TPMS. */
|
||||
&SchraderTPMSDecoder, /* Schrader TPMS. */
|
||||
&SchraderEG53MA4TPMSDecoder, /* Schrader EG53MA4 TPMS. */
|
||||
&CitroenTPMSDecoder, /* Citroen TPMS. */
|
||||
&FordTPMSDecoder, /* Ford TPMS. */
|
||||
&KeeloqDecoder, /* Keeloq remote. */
|
||||
NULL};
|
||||
|
||||
/* Free the message info and allocated data. */
|
||||
void free_msg_info(ProtoViewMsgInfo* i) {
|
||||
if(i == NULL) return;
|
||||
fieldset_free(i->fieldset);
|
||||
free(i->bits);
|
||||
free(i);
|
||||
}
|
||||
@@ -571,6 +614,7 @@ void init_msg_info(ProtoViewMsgInfo* i, ProtoViewApp* app) {
|
||||
UNUSED(app);
|
||||
memset(i, 0, sizeof(ProtoViewMsgInfo));
|
||||
i->bits = NULL;
|
||||
i->fieldset = fieldset_new();
|
||||
}
|
||||
|
||||
/* This function is called when a new signal is detected. It converts it
|
||||
@@ -615,22 +659,17 @@ bool decode_signal(RawSamplesBuffer* s, uint64_t len, ProtoViewMsgInfo* info) {
|
||||
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;
|
||||
if(decoded) {
|
||||
info->decoder = Decoders[j];
|
||||
break;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
if(!decoded) {
|
||||
FURI_LOG_E(TAG, "No decoding possible");
|
||||
} else {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
"Decoded %s, raw=%s info=[%s,%s,%s,%s]",
|
||||
info->name,
|
||||
info->raw,
|
||||
info->info1,
|
||||
info->info2,
|
||||
info->info3,
|
||||
info->info4);
|
||||
FURI_LOG_E(TAG, "+++ Decoded %s", info->decoder->name);
|
||||
/* The message was correctly decoded: fill the info structure
|
||||
* with the decoded signal. The decoder may not implement offset/len
|
||||
* filling of the structure. In such case we have no info and
|
||||
|
||||
248
applications/plugins/protoview/view_build.c
Normal file
248
applications/plugins/protoview/view_build.c
Normal file
@@ -0,0 +1,248 @@
|
||||
/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
|
||||
* See the LICENSE file for information about the license. */
|
||||
|
||||
#include "app.h"
|
||||
|
||||
extern ProtoViewDecoder* Decoders[]; // Defined in signal.c.
|
||||
|
||||
/* Our view private data. */
|
||||
#define USER_VALUE_LEN 64
|
||||
typedef struct {
|
||||
ProtoViewDecoder* decoder; /* Decoder we are using to create a
|
||||
message. */
|
||||
uint32_t cur_decoder; /* Decoder index when we are yet selecting
|
||||
a decoder. Used when decoder is NULL. */
|
||||
ProtoViewFieldSet* fieldset; /* The fields to populate. */
|
||||
uint32_t cur_field; /* Field we are editing right now. This
|
||||
is the index inside the 'fieldset'
|
||||
fields. */
|
||||
char* user_value; /* Keyboard input to replace the current
|
||||
field value goes here. */
|
||||
} BuildViewPrivData;
|
||||
|
||||
/* Not all the decoders support message bulding, so we can't just
|
||||
* increment / decrement the cur_decoder index here. */
|
||||
static void select_next_decoder(ProtoViewApp* app) {
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
do {
|
||||
privdata->cur_decoder++;
|
||||
if(Decoders[privdata->cur_decoder] == NULL) privdata->cur_decoder = 0;
|
||||
} while(Decoders[privdata->cur_decoder]->get_fields == NULL);
|
||||
}
|
||||
|
||||
/* Like select_next_decoder() but goes backward. */
|
||||
static void select_prev_decoder(ProtoViewApp* app) {
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
do {
|
||||
if(privdata->cur_decoder == 0) {
|
||||
/* Go one after the last one to wrap around. */
|
||||
while(Decoders[privdata->cur_decoder]) privdata->cur_decoder++;
|
||||
}
|
||||
privdata->cur_decoder--;
|
||||
} while(Decoders[privdata->cur_decoder]->get_fields == NULL);
|
||||
}
|
||||
|
||||
/* Render the view to select the decoder, among the ones that
|
||||
* support message building. */
|
||||
static void render_view_select_decoder(Canvas* const canvas, ProtoViewApp* app) {
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str(canvas, 0, 9, "Signal creator");
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str(canvas, 0, 19, "up/down: select, ok: choose");
|
||||
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 64, 38, AlignCenter, AlignCenter, Decoders[privdata->cur_decoder]->name);
|
||||
}
|
||||
|
||||
/* Render the view that allows the user to populate the fields needed
|
||||
* for the selected decoder to build a message. */
|
||||
static void render_view_set_fields(Canvas* const canvas, ProtoViewApp* app) {
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
char buf[32];
|
||||
snprintf(
|
||||
buf,
|
||||
sizeof(buf),
|
||||
"%s field %d/%d",
|
||||
privdata->decoder->name,
|
||||
(int)privdata->cur_field + 1,
|
||||
(int)privdata->fieldset->numfields);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(canvas, 0, 0, 128, 21);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str(canvas, 1, 9, buf);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str(canvas, 1, 19, "up/down: next field, ok: edit");
|
||||
|
||||
/* Write the field name, type, current content. */
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
ProtoViewField* field = privdata->fieldset->fields[privdata->cur_field];
|
||||
snprintf(
|
||||
buf, sizeof(buf), "%s %s:%d", field->name, field_get_type_name(field), (int)field->len);
|
||||
buf[0] = toupper(buf[0]);
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str_aligned(canvas, 64, 30, AlignCenter, AlignCenter, buf);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
/* Render the current value between "" */
|
||||
unsigned int written = (unsigned int)field_to_string(buf + 1, sizeof(buf) - 1, field);
|
||||
buf[0] = '"';
|
||||
if(written + 3 < sizeof(buf)) memcpy(buf + written + 1, "\"\x00", 2);
|
||||
canvas_draw_str_aligned(canvas, 63, 45, AlignCenter, AlignCenter, buf);
|
||||
|
||||
/* Footer instructions. */
|
||||
canvas_draw_str(canvas, 0, 62, "Long ok: create, < > incr/decr");
|
||||
}
|
||||
|
||||
/* Render the build message view. */
|
||||
void render_view_build_message(Canvas* const canvas, ProtoViewApp* app) {
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
|
||||
if(privdata->decoder)
|
||||
render_view_set_fields(canvas, app);
|
||||
else
|
||||
render_view_select_decoder(canvas, app);
|
||||
}
|
||||
|
||||
/* Handle input for the decoder selection. */
|
||||
static void process_input_select_decoder(ProtoViewApp* app, InputEvent input) {
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
if(input.type == InputTypeShort) {
|
||||
if(input.key == InputKeyOk) {
|
||||
privdata->decoder = Decoders[privdata->cur_decoder];
|
||||
privdata->fieldset = fieldset_new();
|
||||
privdata->decoder->get_fields(privdata->fieldset);
|
||||
|
||||
/* If the currently decoded message was produced with the
|
||||
* same decoder the user selected, let's populate the
|
||||
* defaults with the current values. So the user will
|
||||
* actaully edit the current message. */
|
||||
if(app->signal_decoded && app->msg_info->decoder == privdata->decoder) {
|
||||
fieldset_copy_matching_fields(privdata->fieldset, app->msg_info->fieldset);
|
||||
}
|
||||
|
||||
/* Now we use the subview system in order to protect the
|
||||
message editing mode from accidental < or > presses.
|
||||
Since we are technically into a subview now, we'll have
|
||||
control of < and >. */
|
||||
InputEvent ii = {.type = InputTypePress, .key = InputKeyDown};
|
||||
ui_process_subview_updown(app, ii, 2);
|
||||
} else if(input.key == InputKeyDown) {
|
||||
select_next_decoder(app);
|
||||
} else if(input.key == InputKeyUp) {
|
||||
select_prev_decoder(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Called after the user typed the new field value in the keyboard.
|
||||
* Let's save it and remove the keyboard view. */
|
||||
static void text_input_done_callback(void* context) {
|
||||
ProtoViewApp* app = context;
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
|
||||
if(field_set_from_string(
|
||||
privdata->fieldset->fields[privdata->cur_field],
|
||||
privdata->user_value,
|
||||
strlen(privdata->user_value)) == false) {
|
||||
ui_show_alert(app, "Invalid value", 1500);
|
||||
}
|
||||
|
||||
free(privdata->user_value);
|
||||
privdata->user_value = NULL;
|
||||
ui_dismiss_keyboard(app);
|
||||
}
|
||||
|
||||
/* Handles the effects of < and > keys in field editing mode.
|
||||
* Instead of force the user to enter the text input mode, delete
|
||||
* the old value, enter the one, we allow to increment and
|
||||
* decrement the current field in a much simpler way.
|
||||
*
|
||||
* The current filed is changed by 'incr' amount. */
|
||||
static bool increment_current_field(ProtoViewApp* app, int incr) {
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
ProtoViewFieldSet* fs = privdata->fieldset;
|
||||
ProtoViewField* f = fs->fields[privdata->cur_field];
|
||||
return field_incr_value(f, incr);
|
||||
}
|
||||
|
||||
/* Handle input for fields editing mode. */
|
||||
static void process_input_set_fields(ProtoViewApp* app, InputEvent input) {
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
ProtoViewFieldSet* fs = privdata->fieldset;
|
||||
|
||||
if(input.type == InputTypeShort && input.key == InputKeyOk) {
|
||||
/* Show the keyboard to let the user type the new
|
||||
* value. */
|
||||
if(privdata->user_value == NULL) privdata->user_value = malloc(USER_VALUE_LEN);
|
||||
field_to_string(privdata->user_value, USER_VALUE_LEN, fs->fields[privdata->cur_field]);
|
||||
ui_show_keyboard(app, privdata->user_value, USER_VALUE_LEN, text_input_done_callback);
|
||||
} else if(input.type == InputTypeShort && input.key == InputKeyDown) {
|
||||
privdata->cur_field = (privdata->cur_field + 1) % fs->numfields;
|
||||
} else if(input.type == InputTypeShort && input.key == InputKeyUp) {
|
||||
if(privdata->cur_field == 0)
|
||||
privdata->cur_field = fs->numfields - 1;
|
||||
else
|
||||
privdata->cur_field--;
|
||||
} else if(input.type == InputTypeShort && input.key == InputKeyRight) {
|
||||
increment_current_field(app, 1);
|
||||
} else if(input.type == InputTypeShort && input.key == InputKeyLeft) {
|
||||
increment_current_field(app, -1);
|
||||
} else if(input.type == InputTypeRepeat && input.key == InputKeyRight) {
|
||||
// The reason why we don't use a large increment directly
|
||||
// is that certain field types only support +1 -1 increments.
|
||||
int times = 10;
|
||||
while(times--) increment_current_field(app, 1);
|
||||
} else if(input.type == InputTypeRepeat && input.key == InputKeyLeft) {
|
||||
int times = 10;
|
||||
while(times--) increment_current_field(app, -1);
|
||||
} else if(input.type == InputTypeLong && input.key == InputKeyOk) {
|
||||
// Build the message in a fresh raw buffer.
|
||||
if(privdata->decoder->build_message) {
|
||||
RawSamplesBuffer* rs = raw_samples_alloc();
|
||||
privdata->decoder->build_message(rs, privdata->fieldset);
|
||||
app->signal_decoded = false; // So that the new signal will be
|
||||
// accepted as the current signal.
|
||||
scan_for_signal(app, rs, 5);
|
||||
raw_samples_free(rs);
|
||||
ui_show_alert(app, "Done: press back key", 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Handle input for the build message view. */
|
||||
void process_input_build_message(ProtoViewApp* app, InputEvent input) {
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
if(privdata->decoder)
|
||||
process_input_set_fields(app, input);
|
||||
else
|
||||
process_input_select_decoder(app, input);
|
||||
}
|
||||
|
||||
/* Enter view callback. */
|
||||
void view_enter_build_message(ProtoViewApp* app) {
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
|
||||
// When we enter the view, the current decoder is just set to zero.
|
||||
// Seek the next valid if needed.
|
||||
if(Decoders[privdata->cur_decoder]->get_fields == NULL) {
|
||||
select_next_decoder(app);
|
||||
}
|
||||
|
||||
// However if there is currently a decoded message, and the
|
||||
// decoder of such message supports message building, let's
|
||||
// select it.
|
||||
if(app->signal_decoded && app->msg_info->decoder->get_fields &&
|
||||
app->msg_info->decoder->build_message) {
|
||||
while(Decoders[privdata->cur_decoder] != app->msg_info->decoder) select_next_decoder(app);
|
||||
}
|
||||
}
|
||||
|
||||
/* Called on exit for cleanup. */
|
||||
void view_exit_build_message(ProtoViewApp* app) {
|
||||
BuildViewPrivData* privdata = app->view_privdata;
|
||||
if(privdata->fieldset) fieldset_free(privdata->fieldset);
|
||||
if(privdata->user_value) free(privdata->user_value);
|
||||
}
|
||||
@@ -2,63 +2,161 @@
|
||||
* See the LICENSE file for information about the license. */
|
||||
|
||||
#include "app.h"
|
||||
|
||||
#include <cc1101.h>
|
||||
|
||||
static void direct_sampling_timer_start(ProtoViewApp* app);
|
||||
static void direct_sampling_timer_stop(ProtoViewApp* app);
|
||||
|
||||
#define CAPTURED_BITMAP_BITS (128 * 64)
|
||||
#define CAPTURED_BITMAP_BYTES (CAPTURED_BITMAP_BITS / 8)
|
||||
#define DEFAULT_USEC_PER_PIXEL 50
|
||||
#define USEC_PER_PIXEL_SMALL_CHANGE 5
|
||||
#define USEC_PER_PIXEL_LARGE_CHANGE 25
|
||||
#define USEC_PER_PIXEL_MIN 5
|
||||
#define USEC_PER_PIXEL_MAX 300
|
||||
typedef struct {
|
||||
uint8_t* captured; // Bitmap with the last captured screen.
|
||||
uint32_t captured_idx; // Current index to write into the bitmap
|
||||
uint32_t usec_per_pixel; // Number of useconds a pixel should represent
|
||||
bool show_usage_info;
|
||||
} DirectSamplingViewPrivData;
|
||||
|
||||
/* 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) {
|
||||
if(!app->direct_sampling_enabled) {
|
||||
DirectSamplingViewPrivData* privdata = app->view_privdata;
|
||||
|
||||
if(!app->direct_sampling_enabled && privdata->show_usage_info) {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str(canvas, 2, 9, "Direct sampling is a special");
|
||||
canvas_draw_str(canvas, 2, 18, "mode that displays the signal");
|
||||
canvas_draw_str(canvas, 2, 27, "captured in real time. Like in");
|
||||
canvas_draw_str(canvas, 2, 36, "a old CRT TV. It's very slow.");
|
||||
canvas_draw_str(canvas, 2, 45, "Can crash your Flipper.");
|
||||
canvas_draw_str(canvas, 2, 9, "Direct sampling displays the");
|
||||
canvas_draw_str(canvas, 2, 18, "the captured signal in real");
|
||||
canvas_draw_str(canvas, 2, 27, "time, like in a CRT TV set.");
|
||||
canvas_draw_str(canvas, 2, 36, "Use UP/DOWN to change the");
|
||||
canvas_draw_str(canvas, 2, 45, "resolution (usec/pixel).");
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str(canvas, 14, 60, "To enable press OK");
|
||||
canvas_draw_str(canvas, 5, 60, "To start/stop, press OK");
|
||||
return;
|
||||
}
|
||||
privdata->show_usage_info = false;
|
||||
|
||||
/* Draw on screen. */
|
||||
int idx = 0;
|
||||
for(int y = 0; y < 64; y++) {
|
||||
for(int x = 0; x < 128; x++) {
|
||||
bool level = furi_hal_gpio_read(&gpio_cc1101_g0);
|
||||
bool level = bitmap_get(privdata->captured, CAPTURED_BITMAP_BYTES, idx++);
|
||||
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 = 250;
|
||||
while(x--)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%lu usec/px", privdata->usec_per_pixel);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_with_border(canvas, 36, 60, "Direct sampling", ColorWhite, ColorBlack);
|
||||
canvas_draw_str_with_border(canvas, 1, 60, buf, ColorWhite, ColorBlack);
|
||||
}
|
||||
|
||||
/* Handle input */
|
||||
void process_input_direct_sampling(ProtoViewApp* app, InputEvent input) {
|
||||
DirectSamplingViewPrivData* privdata = app->view_privdata;
|
||||
|
||||
if(input.type == InputTypePress && input.key == InputKeyOk) {
|
||||
app->direct_sampling_enabled = !app->direct_sampling_enabled;
|
||||
}
|
||||
|
||||
if((input.key == InputKeyUp || input.key == InputKeyDown) &&
|
||||
(input.type == InputTypePress || input.type == InputTypeRepeat)) {
|
||||
uint32_t change = input.type == InputTypePress ? USEC_PER_PIXEL_SMALL_CHANGE :
|
||||
USEC_PER_PIXEL_LARGE_CHANGE;
|
||||
if(input.key == InputKeyUp) change = -change;
|
||||
privdata->usec_per_pixel += change;
|
||||
if(privdata->usec_per_pixel < USEC_PER_PIXEL_MIN)
|
||||
privdata->usec_per_pixel = USEC_PER_PIXEL_MIN;
|
||||
else if(privdata->usec_per_pixel > USEC_PER_PIXEL_MAX)
|
||||
privdata->usec_per_pixel = USEC_PER_PIXEL_MAX;
|
||||
/* Update the timer frequency. */
|
||||
direct_sampling_timer_stop(app);
|
||||
direct_sampling_timer_start(app);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enter view. Stop the subghz thread to prevent access as we read
|
||||
* the CC1101 data directly. */
|
||||
void view_enter_direct_sampling(ProtoViewApp* app) {
|
||||
/* Set view defaults. */
|
||||
DirectSamplingViewPrivData* privdata = app->view_privdata;
|
||||
privdata->usec_per_pixel = DEFAULT_USEC_PER_PIXEL;
|
||||
privdata->captured = malloc(CAPTURED_BITMAP_BYTES);
|
||||
privdata->show_usage_info = true;
|
||||
|
||||
if(app->txrx->txrx_state == TxRxStateRx && !app->txrx->debug_timer_sampling) {
|
||||
subghz_worker_stop(app->txrx->worker);
|
||||
furi_hal_subghz_stop_async_rx();
|
||||
|
||||
/* To read data asynchronously directly from the view, we need
|
||||
* to put the CC1101 back into reception mode (the previous call
|
||||
* to stop the async RX will put it into idle) and configure the
|
||||
* G0 pin for reading. */
|
||||
furi_hal_subghz_rx();
|
||||
furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow);
|
||||
} else {
|
||||
raw_sampling_worker_stop(app);
|
||||
}
|
||||
|
||||
// Start the timer to capture raw data
|
||||
direct_sampling_timer_start(app);
|
||||
}
|
||||
|
||||
/* Exit view. Restore the subghz thread. */
|
||||
void view_exit_direct_sampling(ProtoViewApp* app) {
|
||||
DirectSamplingViewPrivData* privdata = app->view_privdata;
|
||||
if(privdata->captured) free(privdata->captured);
|
||||
app->direct_sampling_enabled = false;
|
||||
|
||||
direct_sampling_timer_stop(app);
|
||||
|
||||
/* Restart normal data feeding. */
|
||||
if(app->txrx->txrx_state == TxRxStateRx && !app->txrx->debug_timer_sampling) {
|
||||
subghz_worker_start(app->txrx->worker);
|
||||
furi_hal_subghz_start_async_rx(protoview_rx_callback, NULL);
|
||||
} else {
|
||||
raw_sampling_worker_start(app);
|
||||
}
|
||||
app->direct_sampling_enabled = false;
|
||||
}
|
||||
|
||||
/* =========================== Timer implementation ========================= */
|
||||
|
||||
static void ds_timer_isr(void* ctx) {
|
||||
ProtoViewApp* app = ctx;
|
||||
DirectSamplingViewPrivData* privdata = app->view_privdata;
|
||||
|
||||
if(app->direct_sampling_enabled) {
|
||||
bool level = furi_hal_gpio_read(&gpio_cc1101_g0);
|
||||
bitmap_set(privdata->captured, CAPTURED_BITMAP_BYTES, privdata->captured_idx, level);
|
||||
privdata->captured_idx = (privdata->captured_idx + 1) % CAPTURED_BITMAP_BITS;
|
||||
}
|
||||
LL_TIM_ClearFlag_UPDATE(TIM2);
|
||||
}
|
||||
|
||||
static void direct_sampling_timer_start(ProtoViewApp* app) {
|
||||
DirectSamplingViewPrivData* privdata = app->view_privdata;
|
||||
|
||||
LL_TIM_InitTypeDef tim_init = {
|
||||
.Prescaler = 63, /* CPU frequency is ~64Mhz. */
|
||||
.CounterMode = LL_TIM_COUNTERMODE_UP,
|
||||
.Autoreload = privdata->usec_per_pixel};
|
||||
|
||||
LL_TIM_Init(TIM2, &tim_init);
|
||||
LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL);
|
||||
LL_TIM_DisableCounter(TIM2);
|
||||
LL_TIM_SetCounter(TIM2, 0);
|
||||
furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, ds_timer_isr, app);
|
||||
LL_TIM_EnableIT_UPDATE(TIM2);
|
||||
LL_TIM_EnableCounter(TIM2);
|
||||
}
|
||||
|
||||
static void direct_sampling_timer_stop(ProtoViewApp* app) {
|
||||
UNUSED(app);
|
||||
FURI_CRITICAL_ENTER();
|
||||
LL_TIM_DisableCounter(TIM2);
|
||||
LL_TIM_DisableIT_UPDATE(TIM2);
|
||||
furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, NULL, NULL);
|
||||
LL_TIM_DeInit(TIM2);
|
||||
FURI_CRITICAL_EXIT();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* See the LICENSE file for information about the license. */
|
||||
|
||||
#include "app.h"
|
||||
#include <gui/view_i.h>
|
||||
#include <gui/view.h>
|
||||
#include <lib/toolbox/random_name.h>
|
||||
|
||||
/* This view has subviews accessible navigating up/down. This
|
||||
@@ -21,33 +21,56 @@ typedef struct {
|
||||
* you can move to next rows. Here we store where we are. */
|
||||
uint32_t signal_display_start_row;
|
||||
char* filename;
|
||||
uint8_t cur_info_page; // Info page to display. Useful when there are
|
||||
// too many fields populated by the decoder that
|
||||
// a single page is not enough.
|
||||
} InfoViewPrivData;
|
||||
|
||||
/* Draw the text label and value of the specified info field at x,y. */
|
||||
static void render_info_field(Canvas* const canvas, ProtoViewField* f, uint8_t x, uint8_t y) {
|
||||
char buf[64];
|
||||
char strval[32];
|
||||
|
||||
field_to_string(strval, sizeof(strval), f);
|
||||
snprintf(buf, sizeof(buf), "%s: %s", f->name, strval);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str(canvas, x, y, buf);
|
||||
}
|
||||
|
||||
/* Render the view with the detected message information. */
|
||||
#define INFO_LINES_PER_PAGE 5
|
||||
static void render_subview_main(Canvas* const canvas, ProtoViewApp* app) {
|
||||
InfoViewPrivData* privdata = app->view_privdata;
|
||||
uint8_t pages =
|
||||
(app->msg_info->fieldset->numfields + (INFO_LINES_PER_PAGE - 1)) / INFO_LINES_PER_PAGE;
|
||||
privdata->cur_info_page %= pages;
|
||||
uint8_t current_page = privdata->cur_info_page;
|
||||
char buf[32];
|
||||
|
||||
/* Protocol name as title. */
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
uint8_t y = 8, lineheight = 10;
|
||||
canvas_draw_str(canvas, 0, y, app->msg_info->name);
|
||||
y += lineheight;
|
||||
|
||||
/* Info fields. */
|
||||
char buf[128];
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
if(app->msg_info->raw[0]) {
|
||||
snprintf(buf, sizeof(buf), "Raw: %s", app->msg_info->raw);
|
||||
if(pages > 1) {
|
||||
snprintf(
|
||||
buf, sizeof(buf), "%s %u/%u", app->msg_info->decoder->name, current_page + 1, pages);
|
||||
canvas_draw_str(canvas, 0, y, buf);
|
||||
y += lineheight;
|
||||
} else {
|
||||
canvas_draw_str(canvas, 0, y, app->msg_info->decoder->name);
|
||||
}
|
||||
canvas_draw_str(canvas, 0, y, app->msg_info->info1);
|
||||
y += lineheight;
|
||||
canvas_draw_str(canvas, 0, y, app->msg_info->info2);
|
||||
y += lineheight;
|
||||
canvas_draw_str(canvas, 0, y, app->msg_info->info3);
|
||||
y += lineheight;
|
||||
canvas_draw_str(canvas, 0, y, app->msg_info->info4);
|
||||
y += lineheight;
|
||||
|
||||
/* Draw the info fields. */
|
||||
uint8_t max_lines = INFO_LINES_PER_PAGE;
|
||||
uint32_t j = current_page * max_lines;
|
||||
while(j < app->msg_info->fieldset->numfields) {
|
||||
render_info_field(canvas, app->msg_info->fieldset->fields[j++], 0, y);
|
||||
y += lineheight;
|
||||
if(--max_lines == 0) break;
|
||||
}
|
||||
|
||||
/* Draw a vertical "save" label. Temporary solution, to switch to
|
||||
* something better ASAP. */
|
||||
y = 37;
|
||||
lineheight = 7;
|
||||
canvas_draw_str(canvas, 119, y, "s");
|
||||
@@ -116,7 +139,7 @@ void render_view_info(Canvas* const canvas, ProtoViewApp* app) {
|
||||
|
||||
/* The user typed the file name. Let's save it and remove the keyboard
|
||||
* view. */
|
||||
void text_input_done_callback(void* context) {
|
||||
static void text_input_done_callback(void* context) {
|
||||
ProtoViewApp* app = context;
|
||||
InfoViewPrivData* privdata = app->view_privdata;
|
||||
|
||||
@@ -126,7 +149,9 @@ void text_input_done_callback(void* context) {
|
||||
furi_string_free(save_path);
|
||||
|
||||
free(privdata->filename);
|
||||
privdata->filename = NULL; // Don't free it again on view exit
|
||||
ui_dismiss_keyboard(app);
|
||||
ui_show_alert(app, "Signal saved", 1500);
|
||||
}
|
||||
|
||||
/* Replace all the occurrences of character c1 with c2 in the specified
|
||||
@@ -143,7 +168,7 @@ void str_replace(char* buf, char c1, char c2) {
|
||||
void set_signal_random_filename(ProtoViewApp* app, char* buf, size_t buflen) {
|
||||
char suffix[6];
|
||||
set_random_name(suffix, sizeof(suffix));
|
||||
snprintf(buf, buflen, "%.10s-%s-%d", app->msg_info->name, suffix, rand() % 1000);
|
||||
snprintf(buf, buflen, "%.10s-%s-%d", app->msg_info->decoder->name, suffix, rand() % 1000);
|
||||
str_replace(buf, ' ', '_');
|
||||
str_replace(buf, '-', '_');
|
||||
str_replace(buf, '/', '_');
|
||||
@@ -270,9 +295,12 @@ void process_input_info(ProtoViewApp* app, InputEvent input) {
|
||||
|
||||
/* Main subview. */
|
||||
if(subview == SubViewInfoMain) {
|
||||
if(input.type == InputTypeShort && input.key == InputKeyOk) {
|
||||
if(input.type == InputTypeLong && input.key == InputKeyOk) {
|
||||
/* Reset the current sample to capture the next. */
|
||||
reset_current_signal(app);
|
||||
} else if(input.type == InputTypeShort && input.key == InputKeyOk) {
|
||||
/* Show next info page. */
|
||||
privdata->cur_info_page++;
|
||||
}
|
||||
} else if(subview == SubViewInfoSave) {
|
||||
/* Save subview. */
|
||||
@@ -281,7 +309,9 @@ void process_input_info(ProtoViewApp* app, InputEvent input) {
|
||||
} else if(input.type == InputTypePress && input.key == InputKeyLeft) {
|
||||
if(privdata->signal_display_start_row != 0) privdata->signal_display_start_row--;
|
||||
} else if(input.type == InputTypeLong && input.key == InputKeyOk) {
|
||||
privdata->filename = malloc(SAVE_FILENAME_LEN);
|
||||
// We have have the buffer already allocated, in case the
|
||||
// user aborted with BACK a previous saving.
|
||||
if(privdata->filename == NULL) privdata->filename = malloc(SAVE_FILENAME_LEN);
|
||||
set_signal_random_filename(app, privdata->filename, SAVE_FILENAME_LEN);
|
||||
ui_show_keyboard(app, privdata->filename, SAVE_FILENAME_LEN, text_input_done_callback);
|
||||
} else if(input.type == InputTypeShort && input.key == InputKeyOk) {
|
||||
@@ -292,3 +322,11 @@ void process_input_info(ProtoViewApp* app, InputEvent input) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Called on view exit. */
|
||||
void view_exit_info(ProtoViewApp* app) {
|
||||
InfoViewPrivData* privdata = app->view_privdata;
|
||||
// When the user aborts the keyboard input, we are left with the
|
||||
// filename buffer allocated.
|
||||
if(privdata->filename) free(privdata->filename);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ void render_view_raw_pulses(Canvas* const canvas, ProtoViewApp* app) {
|
||||
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->msg_info->name, ColorWhite, ColorBlack);
|
||||
canvas_draw_str_with_border(
|
||||
canvas, 1, 61, app->msg_info->decoder->name, ColorWhite, ColorBlack);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,14 +77,15 @@ void process_input_raw_pulses(ProtoViewApp* app, InputEvent input) {
|
||||
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) {
|
||||
} else if(input.type == InputTypeLong) {
|
||||
if(input.key == InputKeyOk) {
|
||||
/* Reset the current sample to capture the next. */
|
||||
reset_current_signal(app);
|
||||
}
|
||||
} else if(input.type == InputTypeShort) {
|
||||
if(input.key == InputKeyOk) {
|
||||
app->signal_offset = 0;
|
||||
adjust_raw_view_scale(app, DetectedSamples->short_pulse_dur);
|
||||
} else if(input.key == InputKeyDown) {
|
||||
/* Rescaling. The set becomes finer under 50us per pixel. */
|
||||
uint32_t scale_step = app->us_scale >= 50 ? 50 : 10;
|
||||
@@ -94,3 +96,19 @@ void process_input_raw_pulses(ProtoViewApp* app, InputEvent input) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Adjust raw view scale depending on short pulse duration. */
|
||||
void adjust_raw_view_scale(ProtoViewApp* app, uint32_t short_pulse_dur) {
|
||||
if(short_pulse_dur == 0)
|
||||
app->us_scale = PROTOVIEW_RAW_VIEW_DEFAULT_SCALE;
|
||||
else if(short_pulse_dur < 75)
|
||||
app->us_scale = 10;
|
||||
else if(short_pulse_dur < 145)
|
||||
app->us_scale = 30;
|
||||
else if(short_pulse_dur < 400)
|
||||
app->us_scale = 100;
|
||||
else if(short_pulse_dur < 1000)
|
||||
app->us_scale = 200;
|
||||
else
|
||||
app->us_scale = PROTOVIEW_RAW_VIEW_DEFAULT_SCALE;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user