├── LICENSE ├── README.md ├── app.c ├── app.h ├── app_subghz.c ├── appicon.png ├── application.fam ├── binaries ├── README.md ├── protoview.fap └── update.sh ├── crc.c ├── custom_presets.h ├── fields.c ├── images ├── ProtoViewSignal.jpg ├── protoview_1.jpg └── protoview_2.jpg ├── protocols ├── b4b1.c ├── keeloq.c ├── oregon2.c ├── pvchat.c ├── tpms │ ├── citroen.c │ ├── ford.c │ ├── renault.c │ ├── schrader.c │ ├── schrader_eg53ma4.c │ └── toyota.c └── unknown.c ├── raw_samples.c ├── raw_samples.h ├── signal.c ├── signal_file.c ├── ui.c ├── view_build.c ├── view_direct_sampling.c ├── view_info.c ├── view_raw_signal.c └── view_settings.c /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-2023 Salvatore Sanfilippo 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 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): 2 | 3 | ![ProtoView screenshot raw signal](/images/protoview_1.jpg) 4 | 5 | This is often enough to make an initial idea about the encoding used 6 | and if the selected modulation is correct. For example, in the signal above 7 | you can see a set of regular pulses and gaps used for synchronization, and then 8 | 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. 9 | 10 | ## Decoding capabilities 11 | 12 | Other than showing the raw signal, ProtoView is able to decode a few interesting protocols: 13 | 14 | * TPMS sensors: Renault, Toyota, Schrader, Citroen, Ford. 15 | * Microchip HSC200/300/301 Keeloq protocol. 16 | * Oregon thermometer protocol 2. 17 | * PT2262, SC5262 based remotes. 18 | * ... more will be implemented soon, hopefully. Send PRs :) 19 | 20 | ![ProtoView screenshot Renault TPMS data](/images/protoview_2.jpg) 21 | 22 | The app implements a framework that makes adding and experimenting with new 23 | protocols very simple. Check the `protocols` directory to see how the 24 | API works, or read the full documentation at the end of this `README` file. 25 | The gist of it is that the decoder receives the signal already converted into 26 | a bitmap, where each bit represents a short pulse duration. Then there are 27 | functions to seek specific sync/preamble sequences inside the bitmap, to decode 28 | from different line codes, to compute checksums and so forth. 29 | 30 | ## Signals transmission capabilities 31 | 32 | Once ProtoView decodes a given message, it is able to *resample* it 33 | in pulses and gaps of the theoretical duration, and send the signal again 34 | via the Flipper TX capabilities. The captured signal can be sent 35 | to different frequencies and modulations than the ones it was captured 36 | from. 37 | 38 | For selected protocols, that implement the message creation methods, 39 | ProtoView is also able to edit the message received, modify fields, 40 | and finally re-detect the new produced signal and resend it. Signals 41 | can also be produced from scratch, by setting all the fields to appropriate 42 | values. 43 | 44 | ## A well-documented app for the Flipper 45 | 46 | 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). 47 | 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. 48 | Unfortunately, this is also true for the firmware of the device. 49 | 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). 50 | 51 | However, the Flipper firmware source code is well written even if it 52 | 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. 53 | 54 | ## Detection algorithm 55 | 56 | 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 57 | or low, and with different duration in microseconds) look like belonging to 58 | a legitimate signal, and aren't just noise. 59 | 60 | We can't make assumptions about 61 | the encoding and the data rate of the communication, so we use a simple 62 | but relatively effective algorithm. As we check the signal, we try to detect 63 | long parts of it that are composed of pulses roughly classifiable into 64 | a maximum of three different duration classes, plus or minus a given percentage. 65 | Most encodings are somewhat self-clocked, so they tend to have just two or 66 | three classes of pulse lengths. 67 | 68 | However, often, pulses of the same theoretical 69 | length have slightly different lengths in the case of high and low level 70 | (RF on or off), so the detector classifies them separately for robustness. 71 | 72 | Once the raw signal is detected, the registered protocol decoders are called 73 | against it, in the hope some of the decoders will make sense of the signal. 74 | 75 | # Usage 76 | 77 | 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. 78 | 79 | ## Main raw signal screen 80 | 81 | * A long press of the OK button resets the current signal, in order to capture a new one. 82 | * The UP and DOWN buttons change the scale. Default is 100us per pixel, but it will be adapted to the signal just captured. 83 | * A long press of the LEFT and RIGHT keys will pan the signal, to see what was transmitted before/after the current shown range. 84 | * A short press to OK will recenter the signal and set the scale back to the default for the specific pulse duration detected. 85 | 86 | Under the detected sequence, you will see a small triangle marking a 87 | specific sample. This mark means that the sequence looked coherent up 88 | to that point, and starting from there it could be just noise. However the 89 | signal decoders will not get just up to this point, but will get more: 90 | sometimes the low level detector can't make sense of a signal that the 91 | protocol-specific decoder can understand fully. 92 | 93 | If the protocol is decoded, the bottom-left corner of the screen 94 | will show the name of the protocol, and going in the next screen 95 | with the right arrow will show information about the decoded signal. 96 | 97 | In the bottom-right corner the application displays an amount of time 98 | in microseconds. This is the average length of the shortest pulse length 99 | detected among the three classes. Usually the *data rate* of the protocol 100 | is something like `1000000/this-number*2`, but it depends on the encoding 101 | and could actually be `1000000/this-number*N` with `N > 2` (here 1000000 102 | is the number of microseconds in one second, and N is the number of clock 103 | cycles needed to represent a bit). 104 | 105 | ## Info screen 106 | 107 | 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: 108 | 109 | 1. Resend the signal, by pressing OK. 110 | 2. Save the signal as `.sub` file, by long pressing OK. 111 | 112 | When resending, you can select a different frequency and modulation if you 113 | wish. 114 | 115 | ## Frequency and modulation screen 116 | 117 | In this view you can just set the frequency and modulation you want to use. 118 | There are special modulations for TPMS signals: they need an higher data 119 | rate. 120 | 121 | * Many cheap remotes (gate openers, remotes, ...) are on the 433.92Mhz or nearby and use OOK modulation. 122 | * Weather stations are often too in the 433.92Mhz OOK. 123 | * For car keys, try 433.92 OOK650 and 868.35 Mhz in OOK or 2FSK. 124 | * 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). 125 | 126 | ## Signal creator 127 | 128 | In this view, you can do two things: 129 | 130 | 1. Select one of the protocols supporting message creation, and create a signal from scratch. 131 | 2. If there is already a detected signal, you can modify the signal. 132 | 133 | This is how it works: 134 | 135 | 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). 136 | 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. 137 | 3. When you are done, long press OK to build the message. Then press BACK in order to see it. 138 | 4. Go to the INFO view, and then DOWN to the signal sending/saving subview in order to send or save it. 139 | 140 | ## Direct sampling screen 141 | 142 | This final screen shows in real time the high and low level that the Flipper 143 | RF chip, the CC1101, is receiving. This will makes very easy to understand 144 | if a given frequency is targeted by something other than noise. This mode is 145 | fun to watch, resembling an old CRT TV set. 146 | 147 | # Installing the app from source 148 | 149 | * Download the Flipper Zero dev kit and build it: 150 | ``` 151 | mkdir -p ~/flipperZero/official/ 152 | cd ~/flipperZero/official/ 153 | git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git ./ 154 | ./fbt 155 | ``` 156 | * Copy this application folder in `official/applications_user`. 157 | * Connect your Flipper via USB. 158 | * Build and install with: `./fbt launch_app APPSRC=protoview`. 159 | 160 | # Installing the binary file (no build needed) 161 | 162 | Drop the `protoview.fap` file you can find in the `binaries` folder into the 163 | following Flipper Zero location: 164 | 165 | /ext/apps/Tools 166 | 167 | The `ext` part means that we are in the SD card. So if you don't want 168 | to use the Android (or other) application to upload the file, 169 | you can just take out the SD card, insert it in your computer, 170 | copy the fine into `apps/Tools`, and that's it. 171 | 172 | # Protocols decoders API 173 | 174 | Writing a protocol decoder is not hard, and requires to write three 175 | different methods. 176 | 177 | 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. 178 | 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. 179 | 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. 180 | 181 | ## `decode()` method 182 | 183 | bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info); 184 | 185 | 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. 186 | 187 | 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: 188 | 189 | 001011 190 | 191 | To access the bitmap, the decoder can use the following API: 192 | 193 | bool bitmap_get(uint8_t *b, uint32_t blen, uint32_t bitpos); 194 | 195 | The `blen` parameter will be set to what the decode method gets 196 | as `numbytes`, and is used to prevent overflows. This way if `bitpos` 197 | is out of range, nothing bad happens. 198 | 199 | There are function to match and seek specific patterns inside the signal: 200 | 201 | bool bitmap_match_bits(uint8_t *b, uint32_t blen, uint32_t bitpos, const char *bits); 202 | uint32_t bitmap_seek_bits(uint8_t *b, uint32_t blen, uint32_t startpos, uint32_t maxbits, const char *bits); 203 | 204 | Finally, there are functions to convert from different line codes: 205 | 206 | 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); 207 | uint32_t convert_from_diff_manchester(uint8_t *buf, uint64_t buflen, uint8_t *bits, uint32_t len, uint32_t off, bool previous); 208 | 209 | This method can also access the short pulse duration by inspecting the 210 | `info->short_pulse_dur` field (in microseconds). 211 | 212 | Please check the `b4b1.c` file for an easy to understand example of the decoder implementation. 213 | 214 | If the decoder actually detected a message, it will return `true` and will return a set of fields, like thata: 215 | 216 | fieldset_add_bytes(info->fieldset,"id",d,5); 217 | fieldset_add_uint(info->fieldset,"button",d[2]&0xf,4); 218 | 219 | ## `get_fields()` method. 220 | 221 | static void get_fields(ProtoViewFieldSet *fieldset); 222 | 223 | This method will be basically a copy of the final part of `decode()`, as 224 | it also needs to return the set of fields this protocol is composed of. 225 | However instead of returning the values of an actual decoded message, it 226 | will just provide their default values for the signal creator view. 227 | 228 | Note that the `build_message()` method is guaranteed to receive the 229 | same exact fields in the same exact order. 230 | 231 | ## `build_message()` method. 232 | 233 | void build_message(RawSamplesBuffer *samples, ProtoViewFieldSet *fs); 234 | 235 | This method is responsible of creating a signal from scratch, by 236 | appending gaps and pulses of the specific duration into `samples` 237 | using the following API: 238 | 239 | raw_samples_add(RawSamplesBuffer *samples, bool level, uint32_t duration); 240 | 241 | Level can be true (pules) or false (gap). Duration is in microseconds. 242 | The method receives a set of fields in `fs`. Each field is accessible 243 | directly accessing `fs->fields[... field index ...]`, where the field 244 | index is 0, 1, 2, ... in the same order as `get_fields()` returned the 245 | fields. 246 | 247 | For now, you can access the fields in the raw way, by getting the 248 | values directly from the data structure representing each field: 249 | 250 | ``` 251 | typedef struct { 252 | ProtoViewFieldType type; 253 | uint32_t len; // Depends on type: 254 | // Bits for integers (signed,unsigned,binary,hex). 255 | // Number of characters for strings. 256 | // Number of nibbles for bytes (1 for each 4 bits). 257 | // Number of digits after dot for floats. 258 | char *name; // Field name. 259 | union { 260 | char *str; // String type. 261 | int64_t value; // Signed integer type. 262 | uint64_t uvalue; // Unsigned integer type. 263 | uint8_t *bytes; // Raw bytes type. 264 | float fvalue; // Float type. 265 | }; 266 | } ProtoViewField; 267 | 268 | ``` 269 | 270 | However later the app will likely provide a set of macros to do it 271 | in a more future-proof way. 272 | 273 | # License 274 | 275 | The code is released under the BSD license. 276 | 277 | # Disclaimer 278 | 279 | This application is only provided as an educational tool. The author is not liable in case the application is used to reverse engineer protocols protected by IP or for any other illegal purpose. 280 | 281 | # Credits 282 | 283 | A big thank you to the RTL433 author, [Benjamin Larsson](https://github.com/merbanan). I used the code and tools he developed in many ways: 284 | * To capture TPMS data with rtl433 and save to a file, to later play the IQ files and speedup the development. 285 | * As a sourve of documentation for protocols. 286 | * As an awesome way to visualize and understand protocols, via [these great web tools](https://triq.org/). 287 | * To have tons of fun with RTLSDR in general, now and in the past. 288 | 289 | The application icon was designed by Stefano Liuzzo. 290 | -------------------------------------------------------------------------------- /app.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | #include "app.h" 5 | 6 | RawSamplesBuffer *RawSamples, *DetectedSamples; 7 | extern const SubGhzProtocolRegistry protoview_protocol_registry; 8 | 9 | /* Draw some text with a border. If the outside color is black and the inside 10 | * color is white, it just writes the border of the text, but the function can 11 | * also be used to write a bold variation of the font setting both the 12 | * colors to black, or alternatively to write a black text with a white 13 | * border so that it is visible if there are black stuff on the background. */ 14 | /* The callback actually just passes the control to the actual active 15 | * view callback, after setting up basic stuff like cleaning the screen 16 | * and setting color to black. */ 17 | static void render_callback(Canvas *const canvas, void *ctx) { 18 | ProtoViewApp *app = ctx; 19 | furi_mutex_acquire(app->view_updating_mutex,FuriWaitForever); 20 | 21 | /* Clear screen. */ 22 | canvas_set_color(canvas, ColorWhite); 23 | canvas_draw_box(canvas, 0, 0, 127, 63); 24 | canvas_set_color(canvas, ColorBlack); 25 | canvas_set_font(canvas, FontPrimary); 26 | 27 | /* Call who is in charge right now. */ 28 | switch(app->current_view) { 29 | case ViewRawPulses: render_view_raw_pulses(canvas,app); break; 30 | case ViewInfo: render_view_info(canvas,app); break; 31 | case ViewFrequencySettings: 32 | case ViewModulationSettings: 33 | render_view_settings(canvas,app); break; 34 | case ViewDirectSampling: render_view_direct_sampling(canvas,app); break; 35 | case ViewBuildMessage: render_view_build_message(canvas,app); break; 36 | default: furi_crash(TAG "Invalid view selected"); break; 37 | } 38 | 39 | /* Draw the alert box if set. */ 40 | ui_draw_alert_if_needed(canvas, app); 41 | furi_mutex_release(app->view_updating_mutex); 42 | } 43 | 44 | /* Here all we do is putting the events into the queue that will be handled 45 | * in the while() loop of the app entry point function. */ 46 | static void input_callback(InputEvent* input_event, void* ctx) 47 | { 48 | ProtoViewApp *app = ctx; 49 | furi_message_queue_put(app->event_queue,input_event,FuriWaitForever); 50 | } 51 | 52 | /* Called to switch view (when left/right is pressed). Handles 53 | * changing the current view ID and calling the enter/exit view 54 | * callbacks if needed. 55 | * 56 | * The 'switchto' parameter can be the identifier of a view, or the 57 | * special views ViewGoNext and ViewGoPrev in order to move to 58 | * the logical next/prev view. */ 59 | static void app_switch_view(ProtoViewApp *app, ProtoViewCurrentView switchto) { 60 | furi_mutex_acquire(app->view_updating_mutex,FuriWaitForever); 61 | 62 | /* Switch to the specified view. */ 63 | ProtoViewCurrentView old = app->current_view; 64 | if (switchto == ViewGoNext) { 65 | app->current_view++; 66 | if (app->current_view == ViewLast) app->current_view = 0; 67 | } else if (switchto == ViewGoPrev) { 68 | if (app->current_view == 0) 69 | app->current_view = ViewLast-1; 70 | else 71 | app->current_view--; 72 | } else { 73 | app->current_view = switchto; 74 | } 75 | ProtoViewCurrentView new = app->current_view; 76 | 77 | /* Call the exit view callbacks. */ 78 | if (old == ViewDirectSampling) view_exit_direct_sampling(app); 79 | if (old == ViewBuildMessage) view_exit_build_message(app); 80 | if (old == ViewInfo) view_exit_info(app); 81 | /* The frequency/modulation settings are actually a single view: 82 | * as long as the user stays between the two modes of this view we 83 | * don't need to call the exit-view callback. */ 84 | if ((old == ViewFrequencySettings && new != ViewModulationSettings) || 85 | (old == ViewModulationSettings && new != ViewFrequencySettings)) 86 | view_exit_settings(app); 87 | 88 | /* Reset the view private data each time, before calling the enter 89 | * callbacks that may want to setup some state. */ 90 | memset(app->view_privdata,0,PROTOVIEW_VIEW_PRIVDATA_LEN); 91 | 92 | /* Call the enter view callbacks after all the exit callback 93 | * of the old view was already executed. */ 94 | if (new == ViewDirectSampling) view_enter_direct_sampling(app); 95 | if (new == ViewBuildMessage) view_enter_build_message(app); 96 | 97 | /* Set the current subview of the view we just left to zero. This is 98 | * the main subview of the old view. When we re-enter the view we are 99 | * lefting, we want to see the main thing again. */ 100 | app->current_subview[old] = 0; 101 | 102 | /* If there is an alert on screen, dismiss it: if the user is 103 | * switching view she already read it. */ 104 | ui_dismiss_alert(app); 105 | furi_mutex_release(app->view_updating_mutex); 106 | } 107 | 108 | /* Allocate the application state and initialize a number of stuff. 109 | * This is called in the entry point to create the application state. */ 110 | ProtoViewApp* protoview_app_alloc() { 111 | ProtoViewApp *app = malloc(sizeof(ProtoViewApp)); 112 | 113 | // Init shared data structures 114 | RawSamples = raw_samples_alloc(); 115 | DetectedSamples = raw_samples_alloc(); 116 | 117 | //init setting 118 | app->setting = subghz_setting_alloc(); 119 | subghz_setting_load(app->setting, EXT_PATH("subghz/assets/setting_user")); 120 | 121 | // GUI 122 | app->gui = furi_record_open(RECORD_GUI); 123 | app->notification = furi_record_open(RECORD_NOTIFICATION); 124 | app->view_port = view_port_alloc(); 125 | view_port_draw_callback_set(app->view_port, render_callback, app); 126 | view_port_input_callback_set(app->view_port, input_callback, app); 127 | gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); 128 | app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); 129 | app->view_dispatcher = NULL; 130 | app->text_input = NULL; 131 | app->show_text_input = false; 132 | app->alert_dismiss_time = 0; 133 | app->current_view = ViewRawPulses; 134 | app->view_updating_mutex = furi_mutex_alloc(FuriMutexTypeNormal); 135 | for (int j = 0; j < ViewLast; j++) app->current_subview[j] = 0; 136 | app->direct_sampling_enabled = false; 137 | app->view_privdata = malloc(PROTOVIEW_VIEW_PRIVDATA_LEN); 138 | memset(app->view_privdata,0,PROTOVIEW_VIEW_PRIVDATA_LEN); 139 | 140 | // Signal found and visualization defaults 141 | app->signal_bestlen = 0; 142 | app->signal_last_scan_idx = 0; 143 | app->signal_decoded = false; 144 | app->us_scale = PROTOVIEW_RAW_VIEW_DEFAULT_SCALE; 145 | app->signal_offset = 0; 146 | app->msg_info = NULL; 147 | 148 | // Init Worker & Protocol 149 | app->txrx = malloc(sizeof(ProtoViewTxRx)); 150 | 151 | /* Setup rx state. */ 152 | app->txrx->freq_mod_changed = false; 153 | app->txrx->debug_timer_sampling = false; 154 | app->txrx->last_g0_change_time = DWT->CYCCNT; 155 | app->txrx->last_g0_value = false; 156 | 157 | app->frequency = subghz_setting_get_default_frequency(app->setting); 158 | app->modulation = 0; /* Defaults to ProtoViewModulations[0]. */ 159 | 160 | furi_hal_power_suppress_charge_enter(); 161 | app->running = 1; 162 | 163 | return app; 164 | } 165 | 166 | /* Free what the application allocated. It is not clear to me if the 167 | * Flipper OS, once the application exits, will be able to reclaim space 168 | * even if we forget to free something here. */ 169 | void protoview_app_free(ProtoViewApp *app) { 170 | furi_assert(app); 171 | 172 | // Put CC1101 on sleep, this also restores charging. 173 | radio_sleep(app); 174 | 175 | // View related. 176 | view_port_enabled_set(app->view_port, false); 177 | gui_remove_view_port(app->gui, app->view_port); 178 | view_port_free(app->view_port); 179 | furi_record_close(RECORD_GUI); 180 | furi_record_close(RECORD_NOTIFICATION); 181 | furi_message_queue_free(app->event_queue); 182 | furi_mutex_free(app->view_updating_mutex); 183 | app->gui = NULL; 184 | 185 | // Frequency setting. 186 | subghz_setting_free(app->setting); 187 | 188 | // Worker stuff. 189 | free(app->txrx); 190 | 191 | // Raw samples buffers. 192 | raw_samples_free(RawSamples); 193 | raw_samples_free(DetectedSamples); 194 | furi_hal_power_suppress_charge_exit(); 195 | 196 | free(app); 197 | } 198 | 199 | /* Called periodically. Do signal processing here. Data we process here 200 | * will be later displayed by the render callback. The side effect of this 201 | * function is to scan for signals and set DetectedSamples. */ 202 | static void timer_callback(void *ctx) { 203 | ProtoViewApp *app = ctx; 204 | uint32_t delta, lastidx = app->signal_last_scan_idx; 205 | 206 | /* scan_for_signal(), called by this function, deals with a 207 | * circular buffer. To never miss anything, even if a signal spawns 208 | * cross-boundaries, it is enough if we scan each time the buffer fills 209 | * for 50% more compared to the last scan. Thanks to this check we 210 | * can avoid scanning too many times to just find the same data. */ 211 | if (lastidx < RawSamples->idx) { 212 | delta = RawSamples->idx - lastidx; 213 | } else { 214 | delta = RawSamples->total - lastidx + RawSamples->idx; 215 | } 216 | if (delta < RawSamples->total/2) return; 217 | app->signal_last_scan_idx = RawSamples->idx; 218 | scan_for_signal(app,RawSamples, 219 | ProtoViewModulations[app->modulation].duration_filter); 220 | } 221 | 222 | /* This is the navigation callback we use in the view dispatcher used 223 | * to display the "text input" widget, that is the keyboard to get text. 224 | * The text input view is implemented to ignore the "back" short press, 225 | * so the event is not consumed and is handled by the view dispatcher. 226 | * However the view dispatcher implementation has the strange behavior that 227 | * if no navigation callback is set, it will not stop when handling back. 228 | * 229 | * We just need a dummy callback returning false. We believe the 230 | * implementation should be changed and if no callback is set, it should be 231 | * the same as returning false. */ 232 | static bool keyboard_view_dispatcher_navigation_callback(void *ctx) { 233 | UNUSED(ctx); 234 | return false; 235 | } 236 | 237 | /* App entry point, as specified in application.fam. */ 238 | int32_t protoview_app_entry(void* p) { 239 | UNUSED(p); 240 | ProtoViewApp *app = protoview_app_alloc(); 241 | 242 | /* Create a timer. We do data analysis in the callback. */ 243 | FuriTimer *timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, app); 244 | furi_timer_start(timer, furi_kernel_get_tick_frequency() / 8); 245 | 246 | /* Start listening to signals immediately. */ 247 | radio_begin(app); 248 | radio_rx(app); 249 | 250 | /* This is the main event loop: here we get the events that are pushed 251 | * in the queue by input_callback(), and process them one after the 252 | * other. The timeout is 100 milliseconds, so if not input is received 253 | * before such time, we exit the queue_get() function and call 254 | * view_port_update() in order to refresh our screen content. */ 255 | InputEvent input; 256 | while(app->running) { 257 | FuriStatus qstat = furi_message_queue_get(app->event_queue, &input, 100); 258 | if (qstat == FuriStatusOk) { 259 | if (DEBUG_MSG) FURI_LOG_E(TAG, "Main Loop - Input: type %d key %u", 260 | input.type, input.key); 261 | 262 | /* Handle navigation here. Then handle view-specific inputs 263 | * in the view specific handling function. */ 264 | if (input.type == InputTypeShort && 265 | input.key == InputKeyBack) 266 | { 267 | if (app->current_view != ViewRawPulses) { 268 | /* If this is not the main app view, go there. */ 269 | app_switch_view(app,ViewRawPulses); 270 | } else { 271 | /* If we are in the main app view, warn the user 272 | * they needs to long press to really quit. */ 273 | ui_show_alert(app,"Long press to exit",1000); 274 | } 275 | } else if (input.type == InputTypeLong && 276 | input.key == InputKeyBack) 277 | { 278 | app->running = 0; 279 | } else if (input.type == InputTypeShort && 280 | input.key == InputKeyRight && 281 | ui_get_current_subview(app) == 0) 282 | { 283 | /* Go to the next view. */ 284 | app_switch_view(app,ViewGoNext); 285 | } else if (input.type == InputTypeShort && 286 | input.key == InputKeyLeft && 287 | ui_get_current_subview(app) == 0) 288 | { 289 | /* Go to the previous view. */ 290 | app_switch_view(app,ViewGoPrev); 291 | } else { 292 | /* This is where we pass the control to the currently 293 | * active view input processing. */ 294 | switch(app->current_view) { 295 | case ViewRawPulses: 296 | process_input_raw_pulses(app,input); 297 | break; 298 | case ViewInfo: 299 | process_input_info(app,input); 300 | break; 301 | case ViewFrequencySettings: 302 | case ViewModulationSettings: 303 | process_input_settings(app,input); 304 | break; 305 | case ViewDirectSampling: 306 | process_input_direct_sampling(app,input); 307 | break; 308 | case ViewBuildMessage: 309 | process_input_build_message(app,input); 310 | break; 311 | default: furi_crash(TAG "Invalid view selected"); break; 312 | } 313 | } 314 | } else { 315 | /* Useful to understand if the app is still alive when it 316 | * does not respond because of bugs. */ 317 | if (DEBUG_MSG) { 318 | static int c = 0; c++; 319 | if (!(c % 20)) FURI_LOG_E(TAG, "Loop timeout"); 320 | } 321 | } 322 | if (app->show_text_input) { 323 | /* Remove our viewport: we need to use a view dispatcher 324 | * in order to show the standard Flipper keyboard. */ 325 | gui_remove_view_port(app->gui, app->view_port); 326 | 327 | /* Allocate a view dispatcher, add a text input view to it, 328 | * and activate it. */ 329 | app->view_dispatcher = view_dispatcher_alloc(); 330 | view_dispatcher_enable_queue(app->view_dispatcher); 331 | /* We need to set a navigation callback for the view dispatcher 332 | * otherwise when the user presses back on the keyboard to 333 | * abort, the dispatcher will not stop. */ 334 | view_dispatcher_set_navigation_event_callback( 335 | app->view_dispatcher, 336 | keyboard_view_dispatcher_navigation_callback); 337 | app->text_input = text_input_alloc(); 338 | view_dispatcher_set_event_callback_context(app->view_dispatcher,app); 339 | view_dispatcher_add_view(app->view_dispatcher, 0, text_input_get_view(app->text_input)); 340 | view_dispatcher_switch_to_view(app->view_dispatcher, 0); 341 | 342 | /* Setup the text input view. The different parameters are set 343 | * in the app structure by the view that wanted to show the 344 | * input text. The callback, buffer and buffer len must be set. */ 345 | text_input_set_header_text(app->text_input, "Save signal filename"); 346 | text_input_set_result_callback( 347 | app->text_input, 348 | app->text_input_done_callback, 349 | app, 350 | app->text_input_buffer, 351 | app->text_input_buffer_len, 352 | false); 353 | 354 | /* Run the dispatcher with the keyboard. */ 355 | view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); 356 | view_dispatcher_run(app->view_dispatcher); 357 | 358 | /* Undo all it: remove the view from the dispatcher, free it 359 | * so that it removes itself from the current gui, finally 360 | * restore our viewport. */ 361 | view_dispatcher_remove_view(app->view_dispatcher, 0); 362 | text_input_free(app->text_input); 363 | view_dispatcher_free(app->view_dispatcher); 364 | app->view_dispatcher = NULL; 365 | gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); 366 | app->show_text_input = false; 367 | } else { 368 | view_port_update(app->view_port); 369 | } 370 | } 371 | 372 | /* App no longer running. Shut down and free. */ 373 | if (app->txrx->txrx_state == TxRxStateRx) { 374 | FURI_LOG_E(TAG, "Putting CC1101 to sleep before exiting."); 375 | radio_rx_end(app); 376 | radio_sleep(app); 377 | } 378 | 379 | furi_timer_free(timer); 380 | protoview_app_free(app); 381 | return 0; 382 | } 383 | 384 | -------------------------------------------------------------------------------- /app.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include "raw_samples.h" 22 | 23 | #define TAG "ProtoView" 24 | #define PROTOVIEW_RAW_VIEW_DEFAULT_SCALE 100 // 100us is 1 pixel by default 25 | #define BITMAP_SEEK_NOT_FOUND UINT32_MAX // Returned by function as sentinel 26 | #define PROTOVIEW_VIEW_PRIVDATA_LEN 64 // View specific private data len 27 | 28 | #define DEBUG_MSG 1 29 | 30 | /* Forward declarations. */ 31 | 32 | typedef struct ProtoViewApp ProtoViewApp; 33 | typedef struct ProtoViewMsgInfo ProtoViewMsgInfo; 34 | typedef struct ProtoViewFieldSet ProtoViewFieldSet; 35 | typedef struct ProtoViewDecoder ProtoViewDecoder; 36 | 37 | /* ============================== enumerations ============================== */ 38 | 39 | /* Subghz system state */ 40 | typedef enum { 41 | TxRxStateIDLE, 42 | TxRxStateRx, 43 | TxRxStateTx, 44 | TxRxStateSleep, 45 | } TxRxState; 46 | 47 | /* Currently active view. */ 48 | typedef enum { 49 | ViewRawPulses, 50 | ViewInfo, 51 | ViewFrequencySettings, 52 | ViewModulationSettings, 53 | ViewBuildMessage, 54 | ViewDirectSampling, 55 | ViewLast, /* Just a sentinel to wrap around. */ 56 | 57 | /* The following are special views that are not iterated, but 58 | * have meaning for the API. */ 59 | ViewGoNext, 60 | ViewGoPrev, 61 | } ProtoViewCurrentView; 62 | 63 | /* ================================== RX/TX ================================= */ 64 | 65 | typedef struct { 66 | const char *name; // Name to show to the user. 67 | const char *id; // Identifier in the Flipper API/file. 68 | FuriHalSubGhzPreset preset; // The preset ID. 69 | uint8_t *custom; /* If not null, a set of registers for 70 | the CC1101, specifying a custom preset.*/ 71 | uint32_t duration_filter; /* Ignore pulses and gaps that are less 72 | than the specified microseconds. This 73 | depends on the data rate. */ 74 | } ProtoViewModulation; 75 | 76 | extern ProtoViewModulation ProtoViewModulations[]; /* In app_subghz.c */ 77 | 78 | /* This is the context of our subghz worker and associated thread. 79 | * It receives data and we get our protocol "feed" callback called 80 | * with the level (1 or 0) and duration. */ 81 | struct ProtoViewTxRx { 82 | bool freq_mod_changed; /* The user changed frequency and/or modulation 83 | from the interface. There is to restart the 84 | radio with the right parameters. */ 85 | TxRxState txrx_state; /* Receiving, idle or sleeping? */ 86 | 87 | /* Timer sampling mode state. */ 88 | bool debug_timer_sampling; /* Read data from GDO0 in a busy loop. Only 89 | for testing. */ 90 | uint32_t last_g0_change_time; /* Last high->low (or reverse) switch. */ 91 | bool last_g0_value; /* Current value (high or low): we are 92 | checking the duration in the timer 93 | handler. */ 94 | }; 95 | 96 | typedef struct ProtoViewTxRx ProtoViewTxRx; 97 | 98 | /* ============================== Main app state ============================ */ 99 | 100 | #define ALERT_MAX_LEN 32 101 | struct ProtoViewApp { 102 | /* GUI */ 103 | Gui *gui; 104 | NotificationApp *notification; 105 | ViewPort *view_port; /* We just use a raw viewport and we render 106 | everything into the low level canvas. */ 107 | ProtoViewCurrentView current_view; /* Active left-right view ID. */ 108 | FuriMutex *view_updating_mutex; /* The Flipper GUI calls the screen redraw 109 | callback in a different thread. We 110 | use this mutex to protect the redraw 111 | from changes in app->view_privdata. */ 112 | int current_subview[ViewLast]; /* Active up-down subview ID. */ 113 | FuriMessageQueue *event_queue; /* Keypress events go here. */ 114 | 115 | /* Input text state. */ 116 | ViewDispatcher *view_dispatcher; /* Used only when we want to show 117 | the text_input view for a moment. 118 | Otherwise it is set to null. */ 119 | TextInput *text_input; 120 | bool show_text_input; 121 | char *text_input_buffer; 122 | uint32_t text_input_buffer_len; 123 | void (*text_input_done_callback)(void*); 124 | 125 | /* Alert state. */ 126 | uint32_t alert_dismiss_time; /* Millisecond when the alert will be 127 | no longer shown. Or zero if the alert 128 | is currently not set at all. */ 129 | char alert_text[ALERT_MAX_LEN]; /* Alert content. */ 130 | 131 | /* Radio related. */ 132 | ProtoViewTxRx *txrx; /* Radio state. */ 133 | SubGhzSetting *setting; /* A list of valid frequencies. */ 134 | 135 | /* Generic app state. */ 136 | int running; /* Once false exists the app. */ 137 | uint32_t signal_bestlen; /* Longest coherent signal observed so far. */ 138 | uint32_t signal_last_scan_idx; /* Index of the buffer last time we 139 | performed the scan. */ 140 | bool signal_decoded; /* Was the current signal decoded? */ 141 | ProtoViewMsgInfo *msg_info; /* Decoded message info if not NULL. */ 142 | bool direct_sampling_enabled; /* This special view needs an explicit 143 | acknowledge to work. */ 144 | void *view_privdata; /* This is a piece of memory of total size 145 | PROTOVIEW_VIEW_PRIVDATA_LEN that it is 146 | initialized to zero when we switch to 147 | a a new view. While the view we are using 148 | is the same, it can be used by the view to 149 | store any kind of info inside, just casting 150 | the pointer to a few specific-data structure. */ 151 | 152 | /* Raw view apps state. */ 153 | uint32_t us_scale; /* microseconds per pixel. */ 154 | uint32_t signal_offset; /* Long press left/right panning in raw view. */ 155 | 156 | /* Configuration view app state. */ 157 | uint32_t frequency; /* Current frequency. */ 158 | uint8_t modulation; /* Current modulation ID, array index in the 159 | ProtoViewModulations table. */ 160 | }; 161 | 162 | /* =========================== Protocols decoders =========================== */ 163 | 164 | /* This stucture is filled by the decoder for specific protocols with the 165 | * informations about the message. ProtoView will display such information 166 | * in the message info view. */ 167 | #define PROTOVIEW_MSG_STR_LEN 32 168 | typedef struct ProtoViewMsgInfo { 169 | ProtoViewDecoder *decoder; /* The decoder that decoded the message. */ 170 | ProtoViewFieldSet *fieldset; /* Decoded fields. */ 171 | /* Low level information of the detected signal: the following are filled 172 | * by the protocol decoding function: */ 173 | uint32_t start_off; /* Pulses start offset in the bitmap. */ 174 | uint32_t pulses_count; /* Number of pulses of the full message. */ 175 | /* The following are passed already filled to the decoder. */ 176 | uint32_t short_pulse_dur; /* Microseconds duration of the short pulse. */ 177 | /* The following are filled by ProtoView core after the decoder returned 178 | * success. */ 179 | uint8_t *bits; /* Bitmap with the signal. */ 180 | uint32_t bits_bytes; /* Number of full bytes in the bitmap, that 181 | is 'pulses_count/8' rounded to the next 182 | integer. */ 183 | } ProtoViewMsgInfo; 184 | 185 | /* This structures describe a set of protocol fields. It is used by decoders 186 | * supporting message building to receive and return information about the 187 | * protocol. */ 188 | typedef enum { 189 | FieldTypeStr, 190 | FieldTypeSignedInt, 191 | FieldTypeUnsignedInt, 192 | FieldTypeBinary, 193 | FieldTypeHex, 194 | FieldTypeBytes, 195 | FieldTypeFloat, 196 | } ProtoViewFieldType; 197 | 198 | typedef struct { 199 | ProtoViewFieldType type; 200 | uint32_t len; // Depends on type: 201 | // Bits for integers (signed,unsigned,binary,hex). 202 | // Number of characters for strings. 203 | // Number of nibbles for bytes (1 for each 4 bits). 204 | // Number of digits after dot for floats. 205 | char *name; // Field name. 206 | union { 207 | char *str; // String type. 208 | int64_t value; // Signed integer type. 209 | uint64_t uvalue; // Unsigned integer type. 210 | uint8_t *bytes; // Raw bytes type. 211 | float fvalue; // Float type. 212 | }; 213 | } ProtoViewField; 214 | 215 | typedef struct ProtoViewFieldSet { 216 | ProtoViewField **fields; 217 | uint32_t numfields; 218 | } ProtoViewFieldSet; 219 | 220 | typedef struct ProtoViewDecoder { 221 | const char *name; /* Protocol name. */ 222 | /* The decode function takes a buffer that is actually a bitmap, with 223 | * high and low levels represented as 0 and 1. The number of high/low 224 | * pulses represented by the bitmap is passed as the 'numbits' argument, 225 | * while 'numbytes' represents the total size of the bitmap pointed by 226 | * 'bits'. So 'numbytes' is mainly useful to pass as argument to other 227 | * functions that perform bit extraction with bound checking, such as 228 | * bitmap_get() and so forth. */ 229 | bool (*decode)(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info); 230 | /* This method is used by the decoder to return the fields it needs 231 | * in order to build a new message. This way the message builder view 232 | * can ask the user to fill the right set of fields of the specified 233 | * type. */ 234 | void (*get_fields)(ProtoViewFieldSet *fields); 235 | /* This method takes the fields supported by the decoder, and 236 | * renders a message in 'samples'. */ 237 | void (*build_message)(RawSamplesBuffer *samples, ProtoViewFieldSet *fields); 238 | } ProtoViewDecoder; 239 | 240 | extern RawSamplesBuffer *RawSamples, *DetectedSamples; 241 | 242 | /* app_subghz.c */ 243 | void radio_begin(ProtoViewApp* app); 244 | uint32_t radio_rx(ProtoViewApp* app); 245 | void radio_idle(ProtoViewApp* app); 246 | void radio_rx_end(ProtoViewApp* app); 247 | void radio_sleep(ProtoViewApp* app); 248 | void raw_sampling_worker_start(ProtoViewApp *app); 249 | void raw_sampling_worker_stop(ProtoViewApp *app); 250 | void radio_tx_signal(ProtoViewApp *app, FuriHalSubGhzAsyncTxCallback data_feeder, void *ctx); 251 | void protoview_rx_callback(bool level, uint32_t duration, void* context); 252 | 253 | /* signal.c */ 254 | uint32_t duration_delta(uint32_t a, uint32_t b); 255 | void reset_current_signal(ProtoViewApp *app); 256 | void scan_for_signal(ProtoViewApp *app,RawSamplesBuffer *source,uint32_t min_duration); 257 | bool bitmap_get(uint8_t *b, uint32_t blen, uint32_t bitpos); 258 | void bitmap_set(uint8_t *b, uint32_t blen, uint32_t bitpos, bool val); 259 | void bitmap_copy(uint8_t *d, uint32_t dlen, uint32_t doff, uint8_t *s, uint32_t slen, uint32_t soff, uint32_t count); 260 | void bitmap_set_pattern(uint8_t *b, uint32_t blen, uint32_t off, const char *pat); 261 | void bitmap_reverse_bytes_bits(uint8_t *p, uint32_t len); 262 | bool bitmap_match_bits(uint8_t *b, uint32_t blen, uint32_t bitpos, const char *bits); 263 | uint32_t bitmap_seek_bits(uint8_t *b, uint32_t blen, uint32_t startpos, uint32_t maxbits, const char *bits); 264 | bool bitmap_match_bitmap(uint8_t *b1, uint32_t b1len, uint32_t b1off, 265 | uint8_t *b2, uint32_t b2len, uint32_t b2off, 266 | uint32_t cmplen); 267 | void bitmap_to_string(char *dst, uint8_t *b, uint32_t blen, 268 | uint32_t off, uint32_t len); 269 | 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); 270 | uint32_t convert_from_diff_manchester(uint8_t *buf, uint64_t buflen, uint8_t *bits, uint32_t len, uint32_t off, bool previous); 271 | void init_msg_info(ProtoViewMsgInfo *i, ProtoViewApp *app); 272 | void free_msg_info(ProtoViewMsgInfo *i); 273 | 274 | /* signal_file.c */ 275 | bool save_signal(ProtoViewApp *app, const char *filename); 276 | 277 | /* view_*.c */ 278 | void render_view_raw_pulses(Canvas *const canvas, ProtoViewApp *app); 279 | void process_input_raw_pulses(ProtoViewApp *app, InputEvent input); 280 | void render_view_settings(Canvas *const canvas, ProtoViewApp *app); 281 | void process_input_settings(ProtoViewApp *app, InputEvent input); 282 | void render_view_info(Canvas *const canvas, ProtoViewApp *app); 283 | void process_input_info(ProtoViewApp *app, InputEvent input); 284 | void render_view_direct_sampling(Canvas *const canvas, ProtoViewApp *app); 285 | void process_input_direct_sampling(ProtoViewApp *app, InputEvent input); 286 | void render_view_build_message(Canvas *const canvas, ProtoViewApp *app); 287 | void process_input_build_message(ProtoViewApp *app, InputEvent input); 288 | void view_enter_build_message(ProtoViewApp *app); 289 | void view_exit_build_message(ProtoViewApp *app); 290 | void view_enter_direct_sampling(ProtoViewApp *app); 291 | void view_exit_direct_sampling(ProtoViewApp *app); 292 | void view_exit_settings(ProtoViewApp *app); 293 | void view_exit_info(ProtoViewApp *app); 294 | void adjust_raw_view_scale(ProtoViewApp *app, uint32_t short_pulse_dur); 295 | 296 | /* ui.c */ 297 | int ui_get_current_subview(ProtoViewApp *app); 298 | void ui_show_available_subviews(Canvas *canvas, ProtoViewApp *app, int last_subview); 299 | bool ui_process_subview_updown(ProtoViewApp *app, InputEvent input, int last_subview); 300 | void ui_show_keyboard(ProtoViewApp *app, char *buffer, uint32_t buflen, 301 | void (*done_callback)(void*)); 302 | void ui_dismiss_keyboard(ProtoViewApp *app); 303 | void ui_show_alert(ProtoViewApp *app, const char *text, uint32_t ttl); 304 | void ui_dismiss_alert(ProtoViewApp *app); 305 | void ui_draw_alert_if_needed(Canvas *canvas, ProtoViewApp *app); 306 | void canvas_draw_str_with_border(Canvas* canvas, uint8_t x, uint8_t y, const char* str, Color text_color, Color border_color); 307 | 308 | /* fields.c */ 309 | void fieldset_free(ProtoViewFieldSet *fs); 310 | ProtoViewFieldSet *fieldset_new(void); 311 | void fieldset_add_int(ProtoViewFieldSet *fs, const char *name, int64_t val, uint8_t bits); 312 | void fieldset_add_uint(ProtoViewFieldSet *fs, const char *name, uint64_t uval, uint8_t bits); 313 | void fieldset_add_hex(ProtoViewFieldSet *fs, const char *name, uint64_t uval, uint8_t bits); 314 | void fieldset_add_bin(ProtoViewFieldSet *fs, const char *name, uint64_t uval, uint8_t bits); 315 | void fieldset_add_str(ProtoViewFieldSet *fs, const char *name, const char *s, size_t len); 316 | void fieldset_add_bytes(ProtoViewFieldSet *fs, const char *name, const uint8_t *bytes, uint32_t count); 317 | void fieldset_add_float(ProtoViewFieldSet *fs, const char *name, float val, uint32_t digits_after_dot); 318 | const char *field_get_type_name(ProtoViewField *f); 319 | int field_to_string(char *buf, size_t len, ProtoViewField *f); 320 | bool field_set_from_string(ProtoViewField *f, char *buf, size_t len); 321 | bool field_incr_value(ProtoViewField *f, int incr); 322 | void fieldset_copy_matching_fields(ProtoViewFieldSet *dst, ProtoViewFieldSet *src); 323 | void field_set_from_field(ProtoViewField *dst, ProtoViewField *src); 324 | 325 | /* crc.c */ 326 | uint8_t crc8(const uint8_t *data, size_t len, uint8_t init, uint8_t poly); 327 | uint8_t sum_bytes(const uint8_t *data, size_t len, uint8_t init); 328 | uint8_t xor_bytes(const uint8_t *data, size_t len, uint8_t init); 329 | -------------------------------------------------------------------------------- /app_subghz.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | #include "app.h" 5 | #include "custom_presets.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | void raw_sampling_timer_start(ProtoViewApp *app); 13 | void raw_sampling_timer_stop(ProtoViewApp *app); 14 | 15 | ProtoViewModulation ProtoViewModulations[] = { 16 | {"OOK 650Khz", "FuriHalSubGhzPresetOok650Async", 17 | FuriHalSubGhzPresetOok650Async, NULL, 30}, 18 | {"OOK 270Khz", "FuriHalSubGhzPresetOok270Async", 19 | FuriHalSubGhzPresetOok270Async, NULL, 30}, 20 | {"2FSK 2.38Khz", "FuriHalSubGhzPreset2FSKDev238Async", 21 | FuriHalSubGhzPreset2FSKDev238Async, NULL, 30}, 22 | {"2FSK 47.6Khz", "FuriHalSubGhzPreset2FSKDev476Async", 23 | FuriHalSubGhzPreset2FSKDev476Async, NULL, 30}, 24 | {"TPMS 1 (FSK)", NULL, 25 | 0, (uint8_t*)protoview_subghz_tpms1_fsk_async_regs, 30}, 26 | {"TPMS 2 (OOK)", NULL, 27 | 0, (uint8_t*)protoview_subghz_tpms2_ook_async_regs, 30}, 28 | {"TPMS 3 (GFSK)", NULL, 29 | 0, (uint8_t*)protoview_subghz_tpms3_gfsk_async_regs, 30}, 30 | {"OOK 40kBaud", NULL, 31 | 0, (uint8_t*)protoview_subghz_40k_ook_async_regs, 15}, 32 | {"FSK 40kBaud", NULL, 33 | 0, (uint8_t*)protoview_subghz_40k_fsk_async_regs, 15}, 34 | {NULL, NULL, 0, NULL, 0} /* End of list sentinel. */ 35 | }; 36 | 37 | /* Called after the application initialization in order to setup the 38 | * subghz system and put it into idle state. */ 39 | void radio_begin(ProtoViewApp* app) { 40 | furi_assert(app); 41 | furi_hal_subghz_reset(); 42 | furi_hal_subghz_idle(); 43 | 44 | /* Power circuits are noisy. Suppressing the charge while we use 45 | * ProtoView will improve the RF performances. */ 46 | furi_hal_power_suppress_charge_enter(); 47 | 48 | /* The CC1101 preset can be either one of the standard presets, if 49 | * the modulation "custom" field is NULL, or a custom preset we 50 | * defined in custom_presets.h. */ 51 | if (ProtoViewModulations[app->modulation].custom == NULL) { 52 | furi_hal_subghz_load_preset( 53 | ProtoViewModulations[app->modulation].preset); 54 | } else { 55 | furi_hal_subghz_load_custom_preset( 56 | ProtoViewModulations[app->modulation].custom); 57 | } 58 | furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow); 59 | app->txrx->txrx_state = TxRxStateIDLE; 60 | } 61 | 62 | /* ================================= Reception ============================== */ 63 | 64 | /* We avoid the subghz provided abstractions and put the data in our 65 | * simple abstraction: the RawSamples circular buffer. */ 66 | void protoview_rx_callback(bool level, uint32_t duration, void *context) { 67 | UNUSED(context); 68 | /* Add data to the circular buffer. */ 69 | raw_samples_add(RawSamples, level, duration); 70 | // FURI_LOG_E(TAG, "FEED: %d %d", (int)level, (int)duration); 71 | return; 72 | } 73 | 74 | /* Setup the CC1101 to start receiving using a background worker. */ 75 | uint32_t radio_rx(ProtoViewApp* app) { 76 | furi_assert(app); 77 | if(!furi_hal_subghz_is_frequency_valid(app->frequency)) { 78 | furi_crash(TAG" Incorrect RX frequency."); 79 | } 80 | 81 | if (app->txrx->txrx_state == TxRxStateRx) return app->frequency; 82 | 83 | furi_hal_subghz_idle(); /* Put it into idle state in case it is sleeping. */ 84 | uint32_t value = furi_hal_subghz_set_frequency_and_path(app->frequency); 85 | FURI_LOG_E(TAG, "Switched to frequency: %lu", value); 86 | furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow); 87 | furi_hal_subghz_flush_rx(); 88 | furi_hal_subghz_rx(); 89 | if (!app->txrx->debug_timer_sampling) { 90 | furi_hal_subghz_start_async_rx(protoview_rx_callback, NULL); 91 | } else { 92 | raw_sampling_worker_start(app); 93 | } 94 | app->txrx->txrx_state = TxRxStateRx; 95 | return value; 96 | } 97 | 98 | /* Stop receiving (if active) and put the radio on idle state. */ 99 | void radio_rx_end(ProtoViewApp* app) { 100 | furi_assert(app); 101 | 102 | if (app->txrx->txrx_state == TxRxStateRx) { 103 | if (!app->txrx->debug_timer_sampling) { 104 | furi_hal_subghz_stop_async_rx(); 105 | } else { 106 | raw_sampling_worker_stop(app); 107 | } 108 | } 109 | furi_hal_subghz_idle(); 110 | app->txrx->txrx_state = TxRxStateIDLE; 111 | } 112 | 113 | /* Put radio on sleep. */ 114 | void radio_sleep(ProtoViewApp* app) { 115 | furi_assert(app); 116 | if (app->txrx->txrx_state == TxRxStateRx) { 117 | /* Stop the asynchronous receiving system before putting the 118 | * chip into sleep. */ 119 | radio_rx_end(app); 120 | } 121 | furi_hal_subghz_sleep(); 122 | app->txrx->txrx_state = TxRxStateSleep; 123 | furi_hal_power_suppress_charge_exit(); 124 | } 125 | 126 | /* =============================== Transmission ============================= */ 127 | 128 | /* This function suspends the current RX state, switches to TX mode, 129 | * transmits the signal provided by the callback data_feeder, and later 130 | * restores the RX state if there was one. */ 131 | void radio_tx_signal(ProtoViewApp *app, FuriHalSubGhzAsyncTxCallback data_feeder, void *ctx) { 132 | TxRxState oldstate = app->txrx->txrx_state; 133 | 134 | if (oldstate == TxRxStateRx) radio_rx_end(app); 135 | radio_begin(app); 136 | 137 | furi_hal_subghz_idle(); 138 | uint32_t value = furi_hal_subghz_set_frequency_and_path(app->frequency); 139 | FURI_LOG_E(TAG, "Switched to frequency: %lu", value); 140 | furi_hal_gpio_write(&gpio_cc1101_g0, false); 141 | furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); 142 | 143 | furi_hal_subghz_start_async_tx(data_feeder, ctx); 144 | while(!furi_hal_subghz_is_async_tx_complete()) furi_delay_ms(10); 145 | furi_hal_subghz_stop_async_tx(); 146 | furi_hal_subghz_idle(); 147 | 148 | radio_begin(app); 149 | if (oldstate == TxRxStateRx) radio_rx(app); 150 | } 151 | 152 | /* ============================= Raw sampling mode ============================= 153 | * This is a special mode that uses a high frequency timer to sample the 154 | * CC1101 pin directly. It's useful for debugging purposes when we want 155 | * to get the raw data from the chip and completely bypass the subghz 156 | * Flipper system. 157 | * ===========================================================================*/ 158 | 159 | void protoview_timer_isr(void *ctx) { 160 | ProtoViewApp *app = ctx; 161 | 162 | bool level = furi_hal_gpio_read(&gpio_cc1101_g0); 163 | if (app->txrx->last_g0_value != level) { 164 | uint32_t now = DWT->CYCCNT; 165 | uint32_t dur = now - app->txrx->last_g0_change_time; 166 | dur /= furi_hal_cortex_instructions_per_microsecond(); 167 | if (dur > 15000) dur = 15000; 168 | raw_samples_add(RawSamples, app->txrx->last_g0_value, dur); 169 | app->txrx->last_g0_value = level; 170 | app->txrx->last_g0_change_time = now; 171 | } 172 | LL_TIM_ClearFlag_UPDATE(TIM2); 173 | } 174 | 175 | void raw_sampling_worker_start(ProtoViewApp *app) { 176 | UNUSED(app); 177 | 178 | LL_TIM_InitTypeDef tim_init = { 179 | .Prescaler = 63, /* CPU frequency is ~64Mhz. */ 180 | .CounterMode = LL_TIM_COUNTERMODE_UP, 181 | .Autoreload = 5, /* Sample every 5 us */ 182 | }; 183 | 184 | LL_TIM_Init(TIM2, &tim_init); 185 | LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL); 186 | LL_TIM_DisableCounter(TIM2); 187 | LL_TIM_SetCounter(TIM2, 0); 188 | furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, protoview_timer_isr, app); 189 | LL_TIM_EnableIT_UPDATE(TIM2); 190 | LL_TIM_EnableCounter(TIM2); 191 | FURI_LOG_E(TAG, "Timer enabled"); 192 | } 193 | 194 | void raw_sampling_worker_stop(ProtoViewApp *app) { 195 | UNUSED(app); 196 | FURI_CRITICAL_ENTER(); 197 | LL_TIM_DisableCounter(TIM2); 198 | LL_TIM_DisableIT_UPDATE(TIM2); 199 | furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, NULL, NULL); 200 | LL_TIM_DeInit(TIM2); 201 | FURI_CRITICAL_EXIT(); 202 | } 203 | -------------------------------------------------------------------------------- /appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antirez/protoview/fd2f6edd43cb813c37a7d912c7dd88f9732d298f/appicon.png -------------------------------------------------------------------------------- /application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="protoview", 3 | name="ProtoView", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="protoview_app_entry", 6 | cdefines=["APP_PROTOVIEW"], 7 | requires=["gui"], 8 | stack_size=8*1024, 9 | order=50, 10 | fap_icon="appicon.png", 11 | fap_category="Tools", 12 | ) 13 | -------------------------------------------------------------------------------- /binaries/README.md: -------------------------------------------------------------------------------- 1 | This is the binary distribution of the application. If you don't want 2 | to build it from source, just take `protoview.fap` file and drop it into the 3 | following Flipper Zero location: 4 | 5 | /ext/apps/Tools 6 | 7 | The `ext` part means that we are in the SD card. So if you don't want 8 | to use the Android (or other) application to upload the file, 9 | you can just take out the SD card, insert it in your computer, 10 | copy the fine into `apps/Tools`, and that's it. 11 | -------------------------------------------------------------------------------- /binaries/protoview.fap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antirez/protoview/fd2f6edd43cb813c37a7d912c7dd88f9732d298f/binaries/protoview.fap -------------------------------------------------------------------------------- /binaries/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | BINPATH="/Users/antirez/hack/flipper/official/build/f7-firmware-D/.extapps/protoview.fap" 3 | cp $BINPATH . 4 | git commit -a -m 'Binary file updated.' 5 | -------------------------------------------------------------------------------- /crc.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | #include 5 | #include 6 | 7 | /* CRC8 with the specified initialization value 'init' and 8 | * polynomial 'poly'. */ 9 | uint8_t crc8(const uint8_t *data, size_t len, uint8_t init, uint8_t poly) 10 | { 11 | uint8_t crc = init; 12 | size_t i, j; 13 | for (i = 0; i < len; i++) { 14 | crc ^= data[i]; 15 | for (j = 0; j < 8; j++) { 16 | if ((crc & 0x80) != 0) 17 | crc = (uint8_t)((crc << 1) ^ poly); 18 | else 19 | crc <<= 1; 20 | } 21 | } 22 | return crc; 23 | } 24 | 25 | /* Sum all the specified bytes modulo 256. 26 | * Initialize the sum with 'init' (usually 0). */ 27 | uint8_t sum_bytes(const uint8_t *data, size_t len, uint8_t init) { 28 | for (size_t i = 0; i < len; i++) init += data[i]; 29 | return init; 30 | } 31 | 32 | /* Perform the bitwise xor of all the specified bytes. 33 | * Initialize xor value with 'init' (usually 0). */ 34 | uint8_t xor_bytes(const uint8_t *data, size_t len, uint8_t init) { 35 | for (size_t i = 0; i < len; i++) init ^= data[i]; 36 | return init; 37 | } 38 | -------------------------------------------------------------------------------- /custom_presets.h: -------------------------------------------------------------------------------- 1 | #include 2 | /* ========================== DATA RATE SETTINGS =============================== 3 | * 4 | * This is how to configure registers MDMCFG3 and MDMCFG4. 5 | * 6 | * MDMCFG3 is the data rate mantissa, the exponent is in MDMCFG4, 7 | * last 4 bits of the register. 8 | * 9 | * The rate (assuming 26Mhz crystal) is calculated as follows: 10 | * 11 | * ((256+MDMCFG3)*(2^MDMCFG4:0..3bits)) / 2^28 * 26000000. 12 | * 13 | * For instance for the default values of MDMCFG3[0..3] (34) and MDMCFG4 (12): 14 | * 15 | * ((256+34)*(2^12))/(2^28)*26000000 = 115051.2688000000, that is 115KBaud 16 | * 17 | * ============================ BANDWIDTH FILTER =============================== 18 | * 19 | * Bandwidth filter setting: 20 | * 21 | * BW filter as just 16 possibilities depending on how the first nibble 22 | * (first 4 bits) of the MDMCFG4 bits are set. Instead of providing the 23 | * formula, it is simpler to show all the values of the nibble and the 24 | * corresponding bandwidth filter. 25 | * 26 | * 0 812khz 27 | * 1 650khz 28 | * 2 541khz 29 | * 3 464khz 30 | * 4 406khz 31 | * 5 325khz 32 | * 6 270khz 33 | * 7 232khz 34 | * 8 203khz 35 | * 9 162khz 36 | * a 135khz 37 | * b 116khz 38 | * c 102khz 39 | * d 82 khz 40 | * e 68 khz 41 | * f 58 khz 42 | * 43 | * ============================== FSK DEVIATION ================================ 44 | * 45 | * FSK deviation is controlled by the DEVIATION register. In Ruby: 46 | * 47 | * dev = (26000000.0/2**17)*(8+(deviation&7))*(2**(deviation>>4&7)) 48 | * 49 | * deviation&7 (last three bits) is the deviation mantissa, while 50 | * deviation>>4&7 (bits 6,5,4) are the exponent. 51 | * 52 | * Deviations values according to certain configuration of DEVIATION: 53 | * 54 | * 0x04 -> 2.380371 kHz 55 | * 0x24 -> 9.521484 kHz 56 | * 0x34 -> 19.042969 Khz 57 | * 0x40 -> 25.390625 Khz 58 | * 0x43 -> 34.912109 Khz 59 | * 0x45 -> 41.259765 Khz 60 | * 0x47 -> 47.607422 kHz 61 | */ 62 | 63 | /* 20 KBaud, 2FSK, 28.56 kHz deviation, 325 Khz bandwidth filter. */ 64 | static uint8_t protoview_subghz_tpms1_fsk_async_regs[][2] = { 65 | /* GPIO GD0 */ 66 | {CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input 67 | 68 | /* Frequency Synthesizer Control */ 69 | {CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz 70 | 71 | /* Packet engine */ 72 | {CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening 73 | {CC1101_PKTCTRL1, 0x04}, 74 | 75 | // // Modem Configuration 76 | {CC1101_MDMCFG0, 0x00}, 77 | {CC1101_MDMCFG1, 0x02}, 78 | {CC1101_MDMCFG2, 0x04}, // Format 2-FSK/FM, No preamble/sync, Disable (current optimized). Other code reading TPMS uses GFSK, but should be the same when in RX mode. 79 | {CC1101_MDMCFG3, 0x93}, // Data rate is 20kBaud 80 | {CC1101_MDMCFG4, 0x59}, // Rx bandwidth filter is 325 kHz 81 | {CC1101_DEVIATN, 0x41}, // Deviation 28.56 kHz 82 | 83 | /* Main Radio Control State Machine */ 84 | {CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us) 85 | 86 | /* Frequency Offset Compensation Configuration */ 87 | {CC1101_FOCCFG, 88 | 0x16}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off 89 | 90 | /* Automatic Gain Control */ 91 | {CC1101_AGCCTRL0, 92 | 0x91}, //10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary 93 | {CC1101_AGCCTRL1, 94 | 0x00}, // 0; 0 - LNA 2 gain is decreased to minimum before decreasing LNA gain; 00 - Relative carrier sense threshold disabled; 0000 - RSSI to MAIN_TARGET 95 | {CC1101_AGCCTRL2, 0x07}, // 00 - DVGA all; 000 - MAX LNA+LNA2; 111 - MAIN_TARGET 42 dB 96 | 97 | /* Wake on radio and timeouts control */ 98 | {CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours 99 | 100 | /* Frontend configuration */ 101 | {CC1101_FREND0, 0x10}, // Adjusts current TX LO buffer 102 | {CC1101_FREND1, 0x56}, 103 | 104 | /* End */ 105 | {0, 0}, 106 | 107 | /* CC1101 2FSK PATABLE. */ 108 | {0xC0, 0}, {0,0}, {0,0}, {0,0} 109 | }; 110 | 111 | /* This is like the default Flipper OOK 640Khz bandwidth preset, but 112 | * the bandwidth is changed to 10kBaud to accomodate TPMS frequency. */ 113 | static const uint8_t protoview_subghz_tpms2_ook_async_regs[][2] = { 114 | /* GPIO GD0 */ 115 | {CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input 116 | 117 | /* FIFO and internals */ 118 | {CC1101_FIFOTHR, 0x07}, // The only important bit is ADC_RETENTION 119 | 120 | /* Packet engine */ 121 | {CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening 122 | 123 | /* Frequency Synthesizer Control */ 124 | {CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz 125 | 126 | // Modem Configuration 127 | {CC1101_MDMCFG0, 0x00}, // Channel spacing is 25kHz 128 | {CC1101_MDMCFG1, 0x00}, // Channel spacing is 25kHz 129 | {CC1101_MDMCFG2, 0x30}, // Format ASK/OOK, No preamble/sync 130 | {CC1101_MDMCFG3, 0x93}, // Data rate is 10kBaud 131 | {CC1101_MDMCFG4, 0x18}, // Rx BW filter is 650.000kHz 132 | 133 | /* Main Radio Control State Machine */ 134 | {CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us) 135 | 136 | /* Frequency Offset Compensation Configuration */ 137 | {CC1101_FOCCFG, 138 | 0x18}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off 139 | 140 | /* Automatic Gain Control */ 141 | {CC1101_AGCCTRL0, 142 | 0x91}, // 10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary 143 | {CC1101_AGCCTRL1, 144 | 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 145 | {CC1101_AGCCTRL2, 0x07}, // 00 - DVGA all; 000 - MAX LNA+LNA2; 111 - MAIN_TARGET 42 dB 146 | 147 | /* Wake on radio and timeouts control */ 148 | {CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours 149 | 150 | /* Frontend configuration */ 151 | {CC1101_FREND0, 0x11}, // Adjusts current TX LO buffer + high is PATABLE[1] 152 | {CC1101_FREND1, 0xB6}, // 153 | 154 | /* End */ 155 | {0, 0}, 156 | 157 | /* CC1101 OOK PATABLE. */ 158 | {0, 0xC0}, {0,0}, {0,0}, {0,0} 159 | }; 160 | 161 | /* GFSK 19k dev, 325 Khz filter, 20kBaud. Different AGI settings. 162 | * Works well with Toyota. */ 163 | static uint8_t protoview_subghz_tpms3_gfsk_async_regs[][2] = { 164 | /* GPIO GD0 */ 165 | {CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input 166 | 167 | /* Frequency Synthesizer Control */ 168 | {CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz 169 | 170 | /* Packet engine */ 171 | {CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening 172 | {CC1101_PKTCTRL1, 0x04}, 173 | 174 | // // Modem Configuration 175 | {CC1101_MDMCFG0, 0x00}, 176 | {CC1101_MDMCFG1, 0x02}, // 2 is the channel spacing exponet: not used 177 | {CC1101_MDMCFG2, 0x10}, // GFSK without any other check 178 | {CC1101_MDMCFG3, 0x93}, // Data rate is 20kBaud 179 | {CC1101_MDMCFG4, 0x59}, // Rx bandwidth filter is 325 kHz 180 | {CC1101_DEVIATN, 0x34}, // Deviation 19.04 Khz. 181 | 182 | /* Main Radio Control State Machine */ 183 | {CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us) 184 | 185 | /* Frequency Offset Compensation Configuration */ 186 | {CC1101_FOCCFG, 187 | 0x16}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off 188 | 189 | /* Automatic Gain Control */ 190 | {CC1101_AGCCTRL0, 0x80}, 191 | {CC1101_AGCCTRL1, 0x58}, 192 | {CC1101_AGCCTRL2, 0x87}, 193 | 194 | /* Wake on radio and timeouts control */ 195 | {CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours 196 | 197 | /* Frontend configuration */ 198 | {CC1101_FREND0, 0x10}, // Adjusts current TX LO buffer 199 | {CC1101_FREND1, 0x56}, 200 | 201 | /* End */ 202 | {0, 0}, 203 | 204 | /* CC1101 2FSK PATABLE. */ 205 | {0xC0, 0}, {0,0}, {0,0}, {0,0} 206 | }; 207 | 208 | /* 40 KBaud, 2FSK, 28 kHz deviation, 270 Khz bandwidth filter. */ 209 | static uint8_t protoview_subghz_40k_fsk_async_regs[][2] = { 210 | /* GPIO GD0 */ 211 | {CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input 212 | 213 | /* Frequency Synthesizer Control */ 214 | {CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz 215 | 216 | /* Packet engine */ 217 | {CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening 218 | {CC1101_PKTCTRL1, 0x04}, 219 | 220 | // // Modem Configuration 221 | {CC1101_MDMCFG0, 0x00}, 222 | {CC1101_MDMCFG1, 0x02}, 223 | {CC1101_MDMCFG2, 0x04}, // Format 2-FSK/FM, No preamble/sync, Disable (current optimized). Other code reading TPMS uses GFSK, but should be the same when in RX mode. 224 | {CC1101_MDMCFG3, 0x93}, // Data rate is 40kBaud 225 | {CC1101_MDMCFG4, 0x6A}, // 6 = BW filter 270kHz, A = Data rate exp 226 | {CC1101_DEVIATN, 0x41}, // Deviation 28kHz 227 | 228 | /* Main Radio Control State Machine */ 229 | {CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us) 230 | 231 | /* Frequency Offset Compensation Configuration */ 232 | {CC1101_FOCCFG, 233 | 0x16}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off 234 | 235 | /* Automatic Gain Control */ 236 | {CC1101_AGCCTRL0, 237 | 0x91}, //10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary 238 | {CC1101_AGCCTRL1, 239 | 0x00}, // 0; 0 - LNA 2 gain is decreased to minimum before decreasing LNA gain; 00 - Relative carrier sense threshold disabled; 0000 - RSSI to MAIN_TARGET 240 | {CC1101_AGCCTRL2, 0x07}, // 00 - DVGA all; 000 - MAX LNA+LNA2; 111 - MAIN_TARGET 42 dB 241 | 242 | /* Wake on radio and timeouts control */ 243 | {CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours 244 | 245 | /* Frontend configuration */ 246 | {CC1101_FREND0, 0x10}, // Adjusts current TX LO buffer 247 | {CC1101_FREND1, 0x56}, 248 | 249 | /* End */ 250 | {0, 0}, 251 | 252 | /* CC1101 2FSK PATABLE. */ 253 | {0xC0, 0}, {0,0}, {0,0}, {0,0} 254 | }; 255 | 256 | /* This is like the default Flipper OOK 640Khz bandwidth preset, but 257 | * the bandwidth is changed to 40kBaud, in order to receive signals 258 | * with a pulse width ~25us/30us. */ 259 | static const uint8_t protoview_subghz_40k_ook_async_regs[][2] = { 260 | /* GPIO GD0 */ 261 | {CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input 262 | 263 | /* FIFO and internals */ 264 | {CC1101_FIFOTHR, 0x07}, // The only important bit is ADC_RETENTION 265 | 266 | /* Packet engine */ 267 | {CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening 268 | 269 | /* Frequency Synthesizer Control */ 270 | {CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz 271 | 272 | // Modem Configuration 273 | {CC1101_MDMCFG0, 0x00}, // Channel spacing is 25kHz 274 | {CC1101_MDMCFG1, 0x00}, // Channel spacing is 25kHz 275 | {CC1101_MDMCFG2, 0x30}, // Format ASK/OOK, No preamble/sync 276 | {CC1101_MDMCFG3, 0x93}, // Data rate is 40kBaud 277 | {CC1101_MDMCFG4, 0x1A}, // Rx BW filter is 650.000kHz 278 | 279 | /* Main Radio Control State Machine */ 280 | {CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us) 281 | 282 | /* Frequency Offset Compensation Configuration */ 283 | {CC1101_FOCCFG, 284 | 0x18}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off 285 | 286 | /* Automatic Gain Control */ 287 | {CC1101_AGCCTRL0, 288 | 0x91}, // 10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary 289 | {CC1101_AGCCTRL1, 290 | 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 291 | {CC1101_AGCCTRL2, 0x07}, // 00 - DVGA all; 000 - MAX LNA+LNA2; 111 - MAIN_TARGET 42 dB 292 | 293 | /* Wake on radio and timeouts control */ 294 | {CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours 295 | 296 | /* Frontend configuration */ 297 | {CC1101_FREND0, 0x11}, // Adjusts current TX LO buffer + high is PATABLE[1] 298 | {CC1101_FREND1, 0xB6}, // 299 | 300 | /* End */ 301 | {0, 0}, 302 | 303 | /* CC1101 OOK PATABLE. */ 304 | {0, 0xC0}, {0,0}, {0,0}, {0,0} 305 | }; 306 | 307 | -------------------------------------------------------------------------------- /fields.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. 3 | * 4 | * Protocol fields implementation. */ 5 | 6 | #include "app.h" 7 | 8 | /* Create a new field of the specified type. Without populating its 9 | * type-specific value. */ 10 | static ProtoViewField *field_new(ProtoViewFieldType type, const char *name) { 11 | ProtoViewField *f = malloc(sizeof(*f)); 12 | f->type = type; 13 | f->name = strdup(name); 14 | return f; 15 | } 16 | 17 | /* Free only the auxiliary data of a field, used to represent the 18 | * current type. Name and type are not touched. */ 19 | static void field_free_aux_data(ProtoViewField *f) { 20 | switch(f->type) { 21 | case FieldTypeStr: free(f->str); break; 22 | case FieldTypeBytes: free(f->bytes); break; 23 | default: break; // Nothing to free for other types. 24 | } 25 | } 26 | 27 | /* Free a field an associated data. */ 28 | static void field_free(ProtoViewField *f) { 29 | field_free_aux_data(f); 30 | free(f->name); 31 | free(f); 32 | } 33 | 34 | /* Return the type of the field as string. */ 35 | const char *field_get_type_name(ProtoViewField *f) { 36 | switch(f->type) { 37 | case FieldTypeStr: return "str"; 38 | case FieldTypeSignedInt: return "int"; 39 | case FieldTypeUnsignedInt: return "uint"; 40 | case FieldTypeBinary: return "bin"; 41 | case FieldTypeHex: return "hex"; 42 | case FieldTypeBytes: return "bytes"; 43 | case FieldTypeFloat: return "float"; 44 | } 45 | return "unknown"; 46 | } 47 | 48 | /* Set a string representation of the specified field in buf. */ 49 | int field_to_string(char *buf, size_t len, ProtoViewField *f) { 50 | switch(f->type) { 51 | case FieldTypeStr: 52 | return snprintf(buf,len,"%s", f->str); 53 | case FieldTypeSignedInt: 54 | return snprintf(buf,len,"%lld", (long long) f->value); 55 | case FieldTypeUnsignedInt: 56 | return snprintf(buf,len,"%llu", (unsigned long long) f->uvalue); 57 | case FieldTypeBinary: 58 | { 59 | uint64_t test_bit = (1 << (f->len-1)); 60 | uint64_t idx = 0; 61 | while(idx < len-1 && test_bit) { 62 | buf[idx++] = (f->uvalue & test_bit) ? '1' : '0'; 63 | test_bit >>= 1; 64 | } 65 | buf[idx] = 0; 66 | return idx; 67 | } 68 | case FieldTypeHex: 69 | return snprintf(buf, len, "%*llX", (int)(f->len+7)/8, f->uvalue); 70 | case FieldTypeFloat: 71 | return snprintf(buf, len, "%.*f", (int)f->len, (double)f->fvalue); 72 | case FieldTypeBytes: 73 | { 74 | uint64_t idx = 0; 75 | while(idx < len-1 && idx < f->len) { 76 | const char *charset = "0123456789ABCDEF"; 77 | uint32_t nibble = idx & 1 ? 78 | (f->bytes[idx/2] & 0xf) : 79 | (f->bytes[idx/2] >> 4); 80 | buf[idx++] = charset[nibble]; 81 | } 82 | buf[idx] = 0; 83 | return idx; 84 | } 85 | } 86 | return 0; 87 | } 88 | 89 | /* Set the field value from its string representation in 'buf'. 90 | * The field type must already be set and the field should be valid. 91 | * The string represenation 'buf' must be null termianted. Note that 92 | * even when representing binary values containing zero, this values 93 | * are taken as representations, so that would be the string "00" as 94 | * the Bytes type representation. 95 | * 96 | * The function returns true if the filed was successfully set to the 97 | * new value, otherwise if the specified value is invalid for the 98 | * field type, false is returned. */ 99 | bool field_set_from_string(ProtoViewField *f, char *buf, size_t len) { 100 | // Initialize values to zero since the Flipper sscanf() implementation 101 | // is fuzzy... may populate only part of the value. 102 | long long val = 0; 103 | unsigned long long uval = 0; 104 | float fval = 0; 105 | 106 | switch(f->type) { 107 | case FieldTypeStr: 108 | free(f->str); 109 | f->len = len; 110 | f->str = malloc(len+1); 111 | memcpy(f->str,buf,len+1); 112 | break; 113 | case FieldTypeSignedInt: 114 | if (!sscanf(buf,"%lld",&val)) return false; 115 | f->value = val; 116 | break; 117 | case FieldTypeUnsignedInt: 118 | if (!sscanf(buf,"%llu",&uval)) return false; 119 | f->uvalue = uval; 120 | break; 121 | case FieldTypeBinary: 122 | { 123 | uint64_t bit_to_set = (1 << (len-1)); 124 | uint64_t idx = 0; 125 | uval = 0; 126 | while(buf[idx]) { 127 | if (buf[idx] == '1') uval |= bit_to_set; 128 | else if (buf[idx] != '0') return false; 129 | bit_to_set >>= 1; 130 | idx++; 131 | } 132 | f->uvalue = uval; 133 | } 134 | break; 135 | case FieldTypeHex: 136 | if (!sscanf(buf,"%llx",&uval) && 137 | !sscanf(buf,"%llX",&uval)) return false; 138 | f->uvalue = uval; 139 | break; 140 | case FieldTypeFloat: 141 | if (!sscanf(buf,"%f",&fval)) return false; 142 | f->fvalue = fval; 143 | break; 144 | case FieldTypeBytes: 145 | { 146 | if (len > f->len) return false; 147 | uint64_t idx = 0; 148 | while(buf[idx]) { 149 | uint8_t nibble = 0; 150 | char c = toupper(buf[idx]); 151 | if (c >= '0' && c <= '9') nibble = c-'0'; 152 | else if (c >= 'A' && c <= 'F') nibble = 10+(c-'A'); 153 | else return false; 154 | 155 | if (idx & 1) { 156 | f->bytes[idx/2] = 157 | (f->bytes[idx/2] & 0xF0) | nibble; 158 | } else { 159 | f->bytes[idx/2] = 160 | (f->bytes[idx/2] & 0x0F) | (nibble<<4); 161 | } 162 | idx++; 163 | } 164 | buf[idx] = 0; 165 | } 166 | break; 167 | } 168 | return true; 169 | } 170 | 171 | /* Set the 'dst' field to contain a copy of the value of the 'src' 172 | * field. The field name is not modified. */ 173 | void field_set_from_field(ProtoViewField *dst, ProtoViewField *src) { 174 | field_free_aux_data(dst); 175 | dst->type = src->type; 176 | dst->len = src->len; 177 | switch(src->type) { 178 | case FieldTypeStr: 179 | dst->str = strdup(src->str); 180 | break; 181 | case FieldTypeBytes: 182 | dst->bytes = malloc(src->len); 183 | memcpy(dst->bytes,src->bytes,dst->len); 184 | break; 185 | case FieldTypeSignedInt: 186 | dst->value = src->value; 187 | break; 188 | case FieldTypeUnsignedInt: 189 | case FieldTypeBinary: 190 | case FieldTypeHex: 191 | dst->uvalue = src->uvalue; 192 | break; 193 | case FieldTypeFloat: 194 | dst->fvalue = src->fvalue; 195 | break; 196 | } 197 | } 198 | 199 | /* Increment the specified field value of 'incr'. If the field type 200 | * does not support increments false is returned, otherwise the 201 | * action is performed. */ 202 | bool field_incr_value(ProtoViewField *f, int incr) { 203 | switch(f->type) { 204 | case FieldTypeStr: return false; 205 | case FieldTypeSignedInt: { 206 | /* Wrap around depending on the number of bits (f->len) 207 | * the integer was declared to have. */ 208 | int64_t max = (1ULL << (f->len-1))-1; 209 | int64_t min = -max-1; 210 | int64_t v = (int64_t)f->value + incr; 211 | if (v > max) v = min+(v-max-1); 212 | if (v < min) v = max+(v-min+1); 213 | f->value = v; 214 | break; 215 | } 216 | case FieldTypeBinary: 217 | case FieldTypeHex: 218 | case FieldTypeUnsignedInt: { 219 | /* Wrap around like for the unsigned case, but here 220 | * is simpler. */ 221 | uint64_t max = (1ULL << f->len)-1; // Broken for 64 bits. 222 | uint64_t uv = (uint64_t)f->value + incr; 223 | if (uv > max) uv = uv & max; 224 | f->uvalue = uv; 225 | break; 226 | } 227 | case FieldTypeFloat: 228 | f->fvalue += incr; 229 | break; 230 | case FieldTypeBytes: { 231 | // For bytes we only support single unit increments. 232 | if (incr != -1 && incr != 1) return false; 233 | for (int j = f->len-1; j >= 0; j--) { 234 | uint8_t nibble = (j&1) ? (f->bytes[j/2] & 0x0F) : 235 | ((f->bytes[j/2] & 0xF0) >> 4); 236 | 237 | nibble += incr; 238 | nibble &= 0x0F; 239 | 240 | f->bytes[j/2] = (j&1) ? ((f->bytes[j/2] & 0xF0) | nibble) : 241 | ((f->bytes[j/2] & 0x0F) | (nibble<<4)); 242 | 243 | /* Propagate the operation on overflow of this nibble. */ 244 | if ((incr == 1 && nibble == 0) || 245 | (incr == -1 && nibble == 0xf)) 246 | { 247 | continue; 248 | } 249 | break; // Otherwise stop the loop here. 250 | } 251 | break; 252 | } 253 | } 254 | return true; 255 | } 256 | 257 | 258 | /* Free a field set and its contained fields. */ 259 | void fieldset_free(ProtoViewFieldSet *fs) { 260 | for (uint32_t j = 0; j < fs->numfields; j++) 261 | field_free(fs->fields[j]); 262 | free(fs->fields); 263 | free(fs); 264 | } 265 | 266 | /* Allocate and init an empty field set. */ 267 | ProtoViewFieldSet *fieldset_new(void) { 268 | ProtoViewFieldSet *fs = malloc(sizeof(*fs)); 269 | fs->numfields = 0; 270 | fs->fields = NULL; 271 | return fs; 272 | } 273 | 274 | /* Append an already allocated field at the end of the specified field set. */ 275 | static void fieldset_add_field(ProtoViewFieldSet *fs, ProtoViewField *field) { 276 | fs->numfields++; 277 | fs->fields = realloc(fs->fields,sizeof(ProtoViewField*)*fs->numfields); 278 | fs->fields[fs->numfields-1] = field; 279 | } 280 | 281 | /* Allocate and append an integer field. */ 282 | void fieldset_add_int(ProtoViewFieldSet *fs, const char *name, int64_t val, uint8_t bits) { 283 | ProtoViewField *f = field_new(FieldTypeSignedInt,name); 284 | f->value = val; 285 | f->len = bits; 286 | fieldset_add_field(fs,f); 287 | } 288 | 289 | /* Allocate and append an unsigned field. */ 290 | void fieldset_add_uint(ProtoViewFieldSet *fs, const char *name, uint64_t uval, uint8_t bits) { 291 | ProtoViewField *f = field_new(FieldTypeUnsignedInt,name); 292 | f->uvalue = uval; 293 | f->len = bits; 294 | fieldset_add_field(fs,f); 295 | } 296 | 297 | /* Allocate and append a hex field. This is an unsigned number but 298 | * with an hex representation. */ 299 | void fieldset_add_hex(ProtoViewFieldSet *fs, const char *name, uint64_t uval, uint8_t bits) { 300 | ProtoViewField *f = field_new(FieldTypeHex,name); 301 | f->uvalue = uval; 302 | f->len = bits; 303 | fieldset_add_field(fs,f); 304 | } 305 | 306 | /* Allocate and append a bin field. This is an unsigned number but 307 | * with a binary representation. */ 308 | void fieldset_add_bin(ProtoViewFieldSet *fs, const char *name, uint64_t uval, uint8_t bits) { 309 | ProtoViewField *f = field_new(FieldTypeBinary,name); 310 | f->uvalue = uval; 311 | f->len = bits; 312 | fieldset_add_field(fs,f); 313 | } 314 | 315 | /* Allocate and append a string field. The string 's' does not need to point 316 | * to a null terminated string, but must have at least 'len' valid bytes 317 | * starting from the pointer. The field object will be correctly null 318 | * terminated. */ 319 | void fieldset_add_str(ProtoViewFieldSet *fs, const char *name, const char *s, size_t len) { 320 | ProtoViewField *f = field_new(FieldTypeStr,name); 321 | f->len = len; 322 | f->str = malloc(len+1); 323 | memcpy(f->str,s,len); 324 | f->str[len] = 0; 325 | fieldset_add_field(fs,f); 326 | } 327 | 328 | /* Allocate and append a bytes field. Note that 'count' is specified in 329 | * nibbles (bytes*2). */ 330 | void fieldset_add_bytes(ProtoViewFieldSet *fs, const char *name, const uint8_t *bytes, uint32_t count_nibbles) { 331 | uint32_t numbytes = (count_nibbles+count_nibbles%2)/2; 332 | ProtoViewField *f = field_new(FieldTypeBytes,name); 333 | f->bytes = malloc(numbytes); 334 | memcpy(f->bytes,bytes,numbytes); 335 | f->len = count_nibbles; 336 | fieldset_add_field(fs,f); 337 | } 338 | 339 | /* Allocate and append a float field. */ 340 | void fieldset_add_float(ProtoViewFieldSet *fs, const char *name, float val, uint32_t digits_after_dot) { 341 | ProtoViewField *f = field_new(FieldTypeFloat,name); 342 | f->fvalue = val; 343 | f->len = digits_after_dot; 344 | fieldset_add_field(fs,f); 345 | } 346 | 347 | /* For each field of the destination filedset 'dst', look for a matching 348 | * field name/type in the source fieldset 'src', and if one is found copy 349 | * its value into the 'dst' field. */ 350 | void fieldset_copy_matching_fields(ProtoViewFieldSet *dst, 351 | ProtoViewFieldSet *src) 352 | { 353 | for (uint32_t j = 0; j < dst->numfields; j++) { 354 | for (uint32_t i = 0; i < src->numfields; i++) { 355 | if (dst->fields[j]->type == src->fields[i]->type && 356 | !strcmp(dst->fields[j]->name,src->fields[i]->name)) 357 | { 358 | field_set_from_field(dst->fields[j], 359 | src->fields[i]); 360 | } 361 | } 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /images/ProtoViewSignal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antirez/protoview/fd2f6edd43cb813c37a7d912c7dd88f9732d298f/images/ProtoViewSignal.jpg -------------------------------------------------------------------------------- /images/protoview_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antirez/protoview/fd2f6edd43cb813c37a7d912c7dd88f9732d298f/images/protoview_1.jpg -------------------------------------------------------------------------------- /images/protoview_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antirez/protoview/fd2f6edd43cb813c37a7d912c7dd88f9732d298f/images/protoview_2.jpg -------------------------------------------------------------------------------- /protocols/b4b1.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. 3 | * 4 | * PT/SC remotes. Usually 443.92 Mhz OOK. 5 | * 6 | * This line code is used in many remotes such as Princeton chips 7 | * named PT2262, Silian Microelectronics SC5262 and others. 8 | * Basically every 4 pulsee represent a bit, where 1000 means 0, and 9 | * 1110 means 1. Usually we can read 24 bits of data. 10 | * In this specific implementation we check for a prelude that is 11 | * 1 bit high, 31 bits low, but the check is relaxed. */ 12 | 13 | #include "../app.h" 14 | 15 | static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) { 16 | if (numbits < 30) return false; 17 | 18 | /* Test different pulse + gap + first byte possibilities. */ 19 | const char *sync_patterns[6] = { 20 | "100000000000000000000000000000011101", /* 30 times gap + one. */ 21 | "100000000000000000000000000000010001", /* 30 times gap + zero. */ 22 | "1000000000000000000000000000000011101", /* 31 times gap + one. */ 23 | "1000000000000000000000000000000010001", /* 31 times gap + zero. */ 24 | "10000000000000000000000000000000011101", /* 32 times gap + one. */ 25 | "10000000000000000000000000000000010001", /* 32 times gap + zero. */ 26 | }; 27 | 28 | uint32_t off; 29 | int j; 30 | for (j = 0; j < 3; j++) { 31 | off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_patterns[j]); 32 | if (off != BITMAP_SEEK_NOT_FOUND) break; 33 | } 34 | if (off == BITMAP_SEEK_NOT_FOUND) return false; 35 | if (DEBUG_MSG) FURI_LOG_E(TAG, "B4B1 preamble id:%d at: %lu",j,off); 36 | info->start_off = off; 37 | 38 | // Seek data setction. Why -5? Last 5 half-bit-times are data. 39 | off += strlen(sync_patterns[j])-5; 40 | 41 | uint8_t d[3]; /* 24 bits of data. */ 42 | uint32_t decoded = 43 | convert_from_line_code(d,sizeof(d),bits,numbytes,off,"1000","1110"); 44 | 45 | if (DEBUG_MSG) FURI_LOG_E(TAG, "B4B1 decoded: %lu",decoded); 46 | if (decoded < 24) return false; 47 | 48 | off += 24*4; // seek to end symbol offset to calculate the length. 49 | off++; // In this protocol there is a final pulse as terminator. 50 | info->pulses_count = off - info->start_off; 51 | 52 | fieldset_add_bytes(info->fieldset,"id",d,5); 53 | fieldset_add_uint(info->fieldset,"button",d[2]&0xf,4); 54 | return true; 55 | } 56 | 57 | /* Give fields and defaults for the signal creator. */ 58 | static void get_fields(ProtoViewFieldSet *fieldset) { 59 | uint8_t default_id[3]= {0xAB, 0xCD, 0xE0}; 60 | fieldset_add_bytes(fieldset,"id",default_id,5); 61 | fieldset_add_uint(fieldset,"button",1,4); 62 | } 63 | 64 | /* Create a signal. */ 65 | static void build_message(RawSamplesBuffer *samples, ProtoViewFieldSet *fs) 66 | { 67 | uint32_t te = 334; // Short pulse duration in microseconds. 68 | 69 | // Sync: 1 te pulse, 31 te gap. 70 | raw_samples_add(samples,true,te); 71 | raw_samples_add(samples,false,te*31); 72 | 73 | // ID + button state 74 | uint8_t data[3]; 75 | memcpy(data,fs->fields[0]->bytes,3); 76 | data[2] = (data[2]&0xF0) | (fs->fields[1]->uvalue & 0xF); 77 | for (uint32_t j = 0; j < 24; j++) { 78 | if (bitmap_get(data,sizeof(data),j)) { 79 | raw_samples_add(samples,true,te*3); 80 | raw_samples_add(samples,false,te); 81 | } else { 82 | raw_samples_add(samples,true,te); 83 | raw_samples_add(samples,false,te*3); 84 | } 85 | } 86 | 87 | // Signal terminator. Just a single short pulse. 88 | raw_samples_add(samples,true,te); 89 | } 90 | 91 | ProtoViewDecoder B4B1Decoder = { 92 | .name = "PT/SC remote", 93 | .decode = decode, 94 | .get_fields = get_fields, 95 | .build_message = build_message 96 | }; 97 | -------------------------------------------------------------------------------- /protocols/keeloq.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. 3 | * 4 | * Microchip HCS200/HCS300/HSC301 KeeLoq, rolling code remotes. 5 | * 6 | * Usually 443.92 Mhz OOK, ~200us or ~400us pulse len, depending 7 | * on the configuration. 8 | * 9 | * Preamble: 12 pairs of alternating pulse/gap. 10 | * Sync: long gap of around 10 times the duration of the short-pulse. 11 | * Data: pulse width encoded data. Each bit takes three cycles: 12 | * 13 | * 0 = 110 14 | * 1 = 100 15 | * 16 | * There are a total of 66 bits transmitted. 17 | * 0..31: 32 bits of encrypted rolling code. 18 | * 32..59: Remote ID, 28 bits 19 | * 60..63: Buttons pressed 20 | * 64..64: Low battery if set 21 | * 65..65: Always set to 1 22 | * 23 | * Bits in bytes are inverted: least significant bit is first. 24 | * For some reason there is no checksum whatsoever, so we only decode 25 | * if we find everything well formed. 26 | */ 27 | 28 | #include "../app.h" 29 | 30 | static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) { 31 | 32 | /* In the sync pattern, we require the 12 high/low pulses and at least 33 | * half the gap we expect (5 pulses times, one is the final zero in the 34 | * 24 symbols high/low sequence, then other 4). */ 35 | const char *sync_pattern = "101010101010101010101010" "0000"; 36 | uint8_t sync_len = 24+4; 37 | if (numbits-sync_len+sync_len < 3*66) return false; 38 | uint32_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern); 39 | if (off == BITMAP_SEEK_NOT_FOUND) return false; 40 | 41 | info->start_off = off; 42 | off += sync_len; // Seek start of message. 43 | 44 | /* Now there is half the gap left, but we allow from 3 to 7, instead of 5 45 | * symbols of gap, to avoid missing the signal for a matter of wrong 46 | * timing. */ 47 | uint8_t gap_len = 0; 48 | while(gap_len <= 7 && bitmap_get(bits,numbytes,off+gap_len) == 0) 49 | gap_len++; 50 | if (gap_len < 3 || gap_len > 7) return false; 51 | 52 | off += gap_len; 53 | FURI_LOG_E(TAG, "Keeloq preamble+sync found"); 54 | 55 | uint8_t raw[9] = {0}; 56 | uint32_t decoded = 57 | convert_from_line_code(raw,sizeof(raw),bits,numbytes,off, 58 | "110","100"); /* Pulse width modulation. */ 59 | FURI_LOG_E(TAG, "Keeloq decoded bits: %lu", decoded); 60 | if (decoded < 66) return false; /* Require the full 66 bits. */ 61 | 62 | info->pulses_count = (off+66*3) - info->start_off; 63 | 64 | bitmap_reverse_bytes_bits(raw,sizeof(raw)); /* Keeloq is LSB first. */ 65 | 66 | int buttons = raw[7]>>4; 67 | int lowbat = (raw[8]&0x1) == 0; // Actual bit meaning: good battery level 68 | int alwaysone = (raw[8]&0x2) != 0; 69 | 70 | fieldset_add_bytes(info->fieldset,"encr",raw,8); 71 | raw[7] = raw[7]<<4; // Make ID bits contiguous 72 | fieldset_add_bytes(info->fieldset,"id",raw+4,7); // 28 bits, 7 nibbles 73 | fieldset_add_bin(info->fieldset,"s[2,1,0,3]",buttons,4); 74 | fieldset_add_bin(info->fieldset,"low battery",lowbat,1); 75 | fieldset_add_bin(info->fieldset,"always one",alwaysone,1); 76 | return true; 77 | } 78 | 79 | static void get_fields(ProtoViewFieldSet *fieldset) { 80 | uint8_t remote_id[4] = {0xab, 0xcd, 0xef, 0xa0}; 81 | uint8_t encr[4] = {0xab, 0xab, 0xab, 0xab}; 82 | fieldset_add_bytes(fieldset,"encr",encr,8); 83 | fieldset_add_bytes(fieldset,"id",remote_id,7); 84 | fieldset_add_bin(fieldset,"s[2,1,0,3]",2,4); 85 | fieldset_add_bin(fieldset,"low battery",0,1); 86 | fieldset_add_bin(fieldset,"always one",1,1); 87 | } 88 | 89 | static void build_message(RawSamplesBuffer *samples, ProtoViewFieldSet *fieldset) 90 | { 91 | uint32_t te = 380; // Short pulse duration in microseconds. 92 | 93 | // Sync: 12 pairs of pulse/gap + 9 times gap 94 | for (int j = 0; j < 12; j++) { 95 | raw_samples_add(samples,true,te); 96 | raw_samples_add(samples,false,te); 97 | } 98 | raw_samples_add(samples,false,te*9); 99 | 100 | // Data, 66 bits. 101 | uint8_t data[9] = {0}; 102 | memcpy(data,fieldset->fields[0]->bytes,4); // Encrypted part. 103 | memcpy(data+4,fieldset->fields[1]->bytes,4); // ID. 104 | data[7] = data[7]>>4 | fieldset->fields[2]->uvalue << 4; // s[2,1,0,3] 105 | int low_battery = fieldset->fields[3] != 0; 106 | int always_one = fieldset->fields[4] != 0; 107 | low_battery = !low_battery; // Bit real meaning is good battery level. 108 | data[8] |= low_battery; 109 | data[8] |= (always_one << 1); 110 | bitmap_reverse_bytes_bits(data,sizeof(data)); /* Keeloq is LSB first. */ 111 | 112 | for (int j = 0; j < 66; j++) { 113 | if (bitmap_get(data,9,j)) { 114 | raw_samples_add(samples,true,te); 115 | raw_samples_add(samples,false,te*2); 116 | } else { 117 | raw_samples_add(samples,true,te*2); 118 | raw_samples_add(samples,false,te); 119 | } 120 | } 121 | } 122 | 123 | ProtoViewDecoder KeeloqDecoder = { 124 | .name = "Keeloq", 125 | .decode = decode, 126 | .get_fields = get_fields, 127 | .build_message = build_message 128 | }; 129 | -------------------------------------------------------------------------------- /protocols/oregon2.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. 3 | * 4 | * Oregon remote termometers. Usually 443.92 Mhz OOK. 5 | * 6 | * The protocol is described here: 7 | * https://wmrx00.sourceforge.net/Arduino/OregonScientific-RF-Protocols.pdf 8 | * This implementation is not very complete. */ 9 | 10 | #include "../app.h" 11 | 12 | static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) { 13 | if (numbits < 32) return false; 14 | const char *sync_pattern = "01100110" "01100110" "10010110" "10010110"; 15 | uint64_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern); 16 | if (off == BITMAP_SEEK_NOT_FOUND) return false; 17 | FURI_LOG_E(TAG, "Oregon2 preamble+sync found"); 18 | 19 | info->start_off = off; 20 | off += 32; /* Skip preamble. */ 21 | 22 | uint8_t buffer[8], raw[8] = {0}; 23 | uint32_t decoded = 24 | convert_from_line_code(buffer,sizeof(buffer),bits,numbytes,off,"1001","0110"); 25 | FURI_LOG_E(TAG, "Oregon2 decoded bits: %lu", decoded); 26 | 27 | if (decoded < 11*4) return false; /* Minimum len to extract some data. */ 28 | info->pulses_count = (off+11*4*4) - info->start_off; 29 | 30 | char temp[3] = {0}, hum[2] = {0}; 31 | uint8_t deviceid[2]; 32 | for (int j = 0; j < 64; j += 4) { 33 | uint8_t nib[1]; 34 | nib[0] = (bitmap_get(buffer,8,j+0) | 35 | bitmap_get(buffer,8,j+1) << 1 | 36 | bitmap_get(buffer,8,j+2) << 2 | 37 | bitmap_get(buffer,8,j+3) << 3); 38 | if (DEBUG_MSG) FURI_LOG_E(TAG, "Not inverted nibble[%d]: %x", j/4, (unsigned int)nib[0]); 39 | raw[j/8] |= nib[0] << (4-(j%4)); 40 | switch(j/4) { 41 | case 1: deviceid[0] |= nib[0]; break; 42 | case 0: deviceid[0] |= nib[0] << 4; break; 43 | case 3: deviceid[1] |= nib[0]; break; 44 | case 2: deviceid[1] |= nib[0] << 4; break; 45 | case 10: temp[0] = nib[0]; break; 46 | /* Fixme: take the temperature sign from nibble 11. */ 47 | case 9: temp[1] = nib[0]; break; 48 | case 8: temp[2] = nib[0]; break; 49 | case 13: hum[0] = nib[0]; break; 50 | case 12: hum[1] = nib[0]; break; 51 | } 52 | } 53 | 54 | float tempval = ((temp[0]-'0')*10) + 55 | (temp[1]-'0') + 56 | ((float)(temp[2]-'0')*0.1); 57 | int humval = (hum[0]-'0')*10 + (hum[1]-'0'); 58 | 59 | fieldset_add_bytes(info->fieldset,"Sensor ID",deviceid,4); 60 | fieldset_add_float(info->fieldset,"Temperature",tempval,1); 61 | fieldset_add_uint(info->fieldset,"Humidity",humval,7); 62 | return true; 63 | } 64 | 65 | ProtoViewDecoder Oregon2Decoder = { 66 | .name = "Oregon2", 67 | .decode = decode, 68 | .get_fields = NULL, 69 | .build_message = NULL 70 | }; 71 | -------------------------------------------------------------------------------- /protocols/pvchat.c: -------------------------------------------------------------------------------- 1 | #include "../app.h" 2 | 3 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 4 | * See the LICENSE file for information about the license. 5 | * 6 | * ---------------------------------------------------------- 7 | * ProtoView chat protocol. This is just a fun test protocol 8 | * that can be used between two Flippers in order to send 9 | * and receive text messages. 10 | * ---------------------------------------------------------- 11 | * 12 | * Protocol description 13 | * ==================== 14 | * 15 | * The protocol works with different data rates. However here is defined 16 | * to use a short pulse/gap duration of 300us and a long pulse/gap 17 | * duration of 600us. Even with the Flipper hardware, the protocol works 18 | * with 100/200us, but becomes less reliable and standard presets can't 19 | * be used because of the higher data rate. 20 | * 21 | * In the following description we have that: 22 | * 23 | * "1" represents a pulse of one-third bit time (300us) 24 | * "0" represents a gap of one-third bit time (300us) 25 | * 26 | * The message starts with a preamble + a sync pattern: 27 | * 28 | * preamble = 1010101010101010 x 3 29 | * sync = 1100110011001010 30 | * 31 | * The a variable amount of bytes follow, where each bit 32 | * is encoded in the following way: 33 | * 34 | * zero 100 (300 us pulse, 600 us gap) 35 | * one 110 (600 us pulse, 300 us gap) 36 | * 37 | * Bytes are sent MSB first, so receiving, in sequence, bits 38 | * 11100001, means byte E1. 39 | * 40 | * This is the data format: 41 | * 42 | * +--+------+-------+--+--+--+ 43 | * |SL|Sender|Message|FF|AA|CS| 44 | * +--+------+-------+--+--+--+ 45 | * | | | 46 | * | | \_ N bytes of message terminated by FF AA + 1 byte of checksum 47 | * | | 48 | * | \_ SL bytes of sender name 49 | * \ 50 | * \_ 1 byte of sender len, 8 bit unsigned integer. 51 | * 52 | * 53 | * Checksum = sum of bytes modulo 256, with checksum set 54 | * to 0 for the computation. 55 | * 56 | * Design notes 57 | * ============ 58 | * 59 | * The protocol is designed in order to have certain properties: 60 | * 61 | * 1. Pulses and gaps can only be 100 or 200 microseconds, so the 62 | * message can be described, encoded and decoded with only two 63 | * fixed durations. 64 | * 65 | * 2. The preamble + sync is designed to have a well recognizable 66 | * pattern that can't be reproduced just for accident inside 67 | * the encoded pattern. There is no combinatio of encoded bits 68 | * leading to the preamble+sync. Also the sync pattern final 69 | * part can't be mistaken for actual bits of data, since it 70 | * contains alternating short pulses/gaps at 100us. 71 | * 72 | * 3. Data encoding wastes some bandwidth in order to be more 73 | * robust. Even so, with a 300us clock period, a single bit 74 | * bit takes 900us, reaching a data transfer of 138 characters per 75 | * second. More than enough for the simple chat we have here. 76 | */ 77 | 78 | static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) { 79 | const char *sync_pattern = "1010101010101010" // Preamble 80 | "1100110011001010"; // Sync 81 | uint8_t sync_len = 32; 82 | 83 | /* This is a variable length message, however the minimum length 84 | * requires a sender len byte (of value zero) and the terminator 85 | * FF 00 plus checksum: a total of 4 bytes. */ 86 | if (numbits-sync_len < 8*4) return false; 87 | 88 | uint64_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern); 89 | if (off == BITMAP_SEEK_NOT_FOUND) return false; 90 | FURI_LOG_E(TAG, "Chat preamble+sync found"); 91 | 92 | /* If there is room on the left, let's mark the start of the message 93 | * a bit before: we don't try to detect all the preamble, but only 94 | * the first part, however it is likely present. */ 95 | if (off >= 16) { 96 | off -= 16; 97 | sync_len += 16; 98 | } 99 | 100 | info->start_off = off; 101 | off += sync_len; /* Skip preamble and sync. */ 102 | 103 | uint8_t raw[64] = {(uint8_t)'.'}; 104 | uint32_t decoded = 105 | convert_from_line_code(raw,sizeof(raw),bits,numbytes,off, 106 | "100","110"); /* PWM */ 107 | FURI_LOG_E(TAG, "Chat decoded bits: %lu", decoded); 108 | 109 | if (decoded < 8*4) return false; /* Min message len. */ 110 | 111 | // The message needs to have a two bytes terminator before 112 | // the checksum. 113 | uint32_t j; 114 | for (j = 0; j < sizeof(raw)-1; j++) 115 | if (raw[j] == 0xff && raw[j+1] == 0xaa) break; 116 | 117 | if (j == sizeof(raw)-1) { 118 | FURI_LOG_E(TAG, "Chat: terminator not found"); 119 | return false; // No terminator found. 120 | } 121 | 122 | uint32_t datalen = j+3; // If the terminator was found at j, then 123 | // we need to sum three more bytes to have 124 | // the len: FF itself, AA, checksum. 125 | info->pulses_count = sync_len + 8*3*datalen; 126 | 127 | // Check if the control sum matches. 128 | if (sum_bytes(raw,datalen-1,0) != raw[datalen-1]) { 129 | FURI_LOG_E(TAG, "Chat: checksum mismatch"); 130 | return false; 131 | } 132 | 133 | // Check if the length of the sender looks sane 134 | uint8_t senderlen = raw[0]; 135 | if (senderlen >= sizeof(raw)) { 136 | FURI_LOG_E(TAG, "Chat: invalid sender length"); 137 | return false; // Overflow 138 | } 139 | 140 | fieldset_add_str(info->fieldset,"sender",(char*)raw+1,senderlen); 141 | fieldset_add_str(info->fieldset,"message",(char*)raw+1+senderlen, 142 | datalen-senderlen-4); 143 | return true; 144 | } 145 | 146 | /* Give fields and defaults for the signal creator. */ 147 | static void get_fields(ProtoViewFieldSet *fieldset) { 148 | fieldset_add_str(fieldset,"sender","Carol",5); 149 | fieldset_add_str(fieldset,"message","Anyone hearing?",15); 150 | } 151 | 152 | /* Create a signal. */ 153 | static void build_message(RawSamplesBuffer *samples, ProtoViewFieldSet *fs) 154 | { 155 | uint32_t te = 300; /* Short pulse duration in microseconds. 156 | Our protocol needs three symbol times to send 157 | a bit, so 300 us per bit = 3.33 kBaud. */ 158 | 159 | // Preamble: 24 alternating 300us pulse/gap pairs. 160 | for (int j = 0; j < 24; j++) { 161 | raw_samples_add(samples,true,te); 162 | raw_samples_add(samples,false,te); 163 | } 164 | 165 | // Sync: 3 alternating 600 us pulse/gap pairs. 166 | for (int j = 0; j < 3; j++) { 167 | raw_samples_add(samples,true,te*2); 168 | raw_samples_add(samples,false,te*2); 169 | } 170 | 171 | // Sync: plus 2 alternating 300 us pluse/gap pairs. 172 | for (int j = 0; j < 2; j++) { 173 | raw_samples_add(samples,true,te); 174 | raw_samples_add(samples,false,te); 175 | } 176 | 177 | // Data: build the array. 178 | uint32_t datalen = 1 + fs->fields[0]->len + // Userlen + Username 179 | fs->fields[1]->len + 3; // Message + FF + 00 + CRC 180 | uint8_t *data = malloc(datalen), *p = data; 181 | *p++ = fs->fields[0]->len; 182 | memcpy(p,fs->fields[0]->str,fs->fields[0]->len); 183 | p += fs->fields[0]->len; 184 | memcpy(p,fs->fields[1]->str,fs->fields[1]->len); 185 | p += fs->fields[1]->len; 186 | *p++ = 0xff; 187 | *p++ = 0xaa; 188 | *p = sum_bytes(data,datalen-1,0); 189 | 190 | // Emit bits 191 | for (uint32_t j = 0; j < datalen*8; j++) { 192 | if (bitmap_get(data,datalen,j)) { 193 | raw_samples_add(samples,true,te*2); 194 | raw_samples_add(samples,false,te); 195 | } else { 196 | raw_samples_add(samples,true,te); 197 | raw_samples_add(samples,false,te*2); 198 | } 199 | } 200 | free(data); 201 | } 202 | 203 | ProtoViewDecoder ProtoViewChatDecoder = { 204 | .name = "ProtoView chat", 205 | .decode = decode, 206 | .get_fields = get_fields, 207 | .build_message = build_message 208 | }; 209 | -------------------------------------------------------------------------------- /protocols/tpms/citroen.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. 3 | * 4 | * Citroen TPMS. Usually 443.92 Mhz FSK. 5 | * 6 | * Preamble of ~14 high/low 52 us pulses 7 | * Sync of high 100us pulse then 50us low 8 | * Then Manchester bits, 10 bytes total. 9 | * Simple XOR checksum. */ 10 | 11 | #include "../../app.h" 12 | 13 | static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) { 14 | 15 | /* We consider a preamble of 17 symbols. They are more, but the decoding 16 | * is more likely to happen if we don't pretend to receive from the 17 | * very start of the message. */ 18 | uint32_t sync_len = 17; 19 | const char *sync_pattern = "10101010101010110"; 20 | if (numbits-sync_len < 8*10) return false; /* Expect 10 bytes. */ 21 | 22 | uint64_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern); 23 | if (off == BITMAP_SEEK_NOT_FOUND) return false; 24 | FURI_LOG_E(TAG, "Renault TPMS preamble+sync found"); 25 | 26 | info->start_off = off; 27 | off += sync_len; /* Skip preamble + sync. */ 28 | 29 | uint8_t raw[10]; 30 | uint32_t decoded = 31 | convert_from_line_code(raw,sizeof(raw),bits,numbytes,off, 32 | "01","10"); /* Manchester. */ 33 | FURI_LOG_E(TAG, "Citroen TPMS decoded bits: %lu", decoded); 34 | 35 | if (decoded < 8*10) return false; /* Require the full 10 bytes. */ 36 | 37 | /* Check the CRC. It's a simple XOR of bytes 1-9, the first byte 38 | * is not included. The meaning of the first byte is unknown and 39 | * we don't display it. */ 40 | uint8_t crc = 0; 41 | for (int j = 1; j < 10; j++) crc ^= raw[j]; 42 | if (crc != 0) return false; /* Require sane checksum. */ 43 | 44 | info->pulses_count = (off+8*10*2) - info->start_off; 45 | 46 | int repeat = raw[5] & 0xf; 47 | float kpa = (float)raw[6]*1.364; 48 | int temp = raw[7]-50; 49 | int battery = raw[8]; /* This may be the battery. It's not clear. */ 50 | 51 | fieldset_add_bytes(info->fieldset,"Tire ID",raw+1,4*2); 52 | fieldset_add_float(info->fieldset,"Pressure kpa",kpa,2); 53 | fieldset_add_int(info->fieldset,"Temperature C",temp,8); 54 | fieldset_add_uint(info->fieldset,"Repeat",repeat,4); 55 | fieldset_add_uint(info->fieldset,"Battery",battery,8); 56 | return true; 57 | } 58 | 59 | ProtoViewDecoder CitroenTPMSDecoder = { 60 | .name = "Citroen TPMS", 61 | .decode = decode, 62 | .get_fields = NULL, 63 | .build_message = NULL 64 | }; 65 | -------------------------------------------------------------------------------- /protocols/tpms/ford.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. 3 | * 4 | * Ford tires TPMS. Usually 443.92 Mhz FSK (in Europe). 5 | * 6 | * 52 us short pules 7 | * Preamble: 0101010101010101010101010101 8 | * Sync: 0110 (that is 52 us gap + 104 us pulse + 52 us gap) 9 | * Data: 8 bytes Manchester encoded 10 | * 01 = zero 11 | * 10 = one 12 | */ 13 | 14 | #include "../../app.h" 15 | 16 | static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) { 17 | 18 | const char *sync_pattern = "010101010101" "0110"; 19 | uint8_t sync_len = 12+4; /* We just use 12 preamble symbols + sync. */ 20 | if (numbits-sync_len < 8*8) return false; 21 | 22 | uint64_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern); 23 | if (off == BITMAP_SEEK_NOT_FOUND) return false; 24 | FURI_LOG_E(TAG, "Fort TPMS preamble+sync found"); 25 | 26 | info->start_off = off; 27 | off += sync_len; /* Skip preamble and sync. */ 28 | 29 | uint8_t raw[8]; 30 | uint32_t decoded = 31 | convert_from_line_code(raw,sizeof(raw),bits,numbytes,off, 32 | "01","10"); /* Manchester. */ 33 | FURI_LOG_E(TAG, "Ford TPMS decoded bits: %lu", decoded); 34 | 35 | if (decoded < 8*8) return false; /* Require the full 8 bytes. */ 36 | 37 | /* CRC is just the sum of the first 7 bytes MOD 256. */ 38 | uint8_t crc = 0; 39 | for (int j = 0; j < 7; j++) crc += raw[j]; 40 | if (crc != raw[7]) return false; /* Require sane CRC. */ 41 | 42 | info->pulses_count = (off+8*8*2) - info->start_off; 43 | 44 | float psi = 0.25 * (((raw[6]&0x20)<<3)|raw[4]); 45 | 46 | /* Temperature apperas to be valid only if the most significant 47 | * bit of the value is not set. Otherwise its meaning is unknown. 48 | * Likely useful to alternatively send temperature or other info. */ 49 | int temp = raw[5] & 0x80 ? 0 : raw[5]-56; 50 | int flags = raw[5] & 0x7f; 51 | int car_moving = (raw[6] & 0x44) == 0x44; 52 | 53 | fieldset_add_bytes(info->fieldset,"Tire ID",raw,4*2); 54 | fieldset_add_float(info->fieldset,"Pressure psi",psi,2); 55 | fieldset_add_int(info->fieldset,"Temperature C",temp,8); 56 | fieldset_add_hex(info->fieldset,"Flags",flags,7); 57 | fieldset_add_uint(info->fieldset,"Moving",car_moving,1); 58 | return true; 59 | } 60 | 61 | ProtoViewDecoder FordTPMSDecoder = { 62 | .name = "Ford TPMS", 63 | .decode = decode, 64 | .get_fields = NULL, 65 | .build_message = NULL 66 | }; 67 | -------------------------------------------------------------------------------- /protocols/tpms/renault.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. 3 | * 4 | * Renault tires TPMS. Usually 443.92 Mhz FSK. 5 | * 6 | * Preamble + sync + Manchester bits. ~48us short pulse. 7 | * 9 Bytes in total not counting the preamble. */ 8 | 9 | #include "../../app.h" 10 | 11 | #define USE_TEST_VECTOR 0 12 | static const char *test_vector = 13 | "...01010101010101010110" // Preamble + sync 14 | 15 | /* The following is Marshal encoded, so each two characters are 16 | * actaully one bit. 01 = 0, 10 = 1. */ 17 | "010110010110" // Flags. 18 | "10011001101010011001" // Pressure, multiply by 0.75 to obtain kpa. 19 | // 244 kpa here. 20 | "1010010110011010" // Temperature, subtract 30 to obtain celsius. 22C here. 21 | "1001010101101001" 22 | "0101100110010101" 23 | "1001010101100110" // Tire ID. 0x7AD779 here. 24 | "0101010101010101" 25 | "0101010101010101" // Two FF bytes (usually). Unknown. 26 | "0110010101010101"; // CRC8 with (poly 7, initialization 0). 27 | 28 | static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) { 29 | 30 | if (USE_TEST_VECTOR) { /* Test vector to check that decoding works. */ 31 | bitmap_set_pattern(bits,numbytes,0,test_vector); 32 | numbits = strlen(test_vector); 33 | } 34 | 35 | if (numbits-12 < 9*8) return false; 36 | 37 | const char *sync_pattern = "01010101010101010110"; 38 | uint64_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern); 39 | if (off == BITMAP_SEEK_NOT_FOUND) return false; 40 | FURI_LOG_E(TAG, "Renault TPMS preamble+sync found"); 41 | 42 | info->start_off = off; 43 | off += 20; /* Skip preamble. */ 44 | 45 | uint8_t raw[9]; 46 | uint32_t decoded = 47 | convert_from_line_code(raw,sizeof(raw),bits,numbytes,off, 48 | "01","10"); /* Manchester. */ 49 | FURI_LOG_E(TAG, "Renault TPMS decoded bits: %lu", decoded); 50 | 51 | if (decoded < 8*9) return false; /* Require the full 9 bytes. */ 52 | if (crc8(raw,8,0,7) != raw[8]) return false; /* Require sane CRC. */ 53 | 54 | info->pulses_count = (off+8*9*2) - info->start_off; 55 | 56 | uint8_t flags = raw[0]>>2; 57 | float kpa = 0.75 * ((uint32_t)((raw[0]&3)<<8) | raw[1]); 58 | int temp = raw[2]-30; 59 | 60 | fieldset_add_bytes(info->fieldset,"Tire ID",raw+3,3*2); 61 | fieldset_add_float(info->fieldset,"Pressure kpa",kpa,2); 62 | fieldset_add_int(info->fieldset,"Temperature C",temp,8); 63 | fieldset_add_hex(info->fieldset,"Flags",flags,6); 64 | fieldset_add_bytes(info->fieldset,"Unknown1",raw+6,2); 65 | fieldset_add_bytes(info->fieldset,"Unknown2",raw+7,2); 66 | return true; 67 | } 68 | 69 | /* Give fields and defaults for the signal creator. */ 70 | static void get_fields(ProtoViewFieldSet *fieldset) { 71 | uint8_t default_id[3]= {0xAB, 0xCD, 0xEF}; 72 | fieldset_add_bytes(fieldset,"Tire ID",default_id,3*2); 73 | fieldset_add_float(fieldset,"Pressure kpa",123,2); 74 | fieldset_add_int(fieldset,"Temperature C",20,8); 75 | // We don't know what flags are, but 1B is a common value. 76 | fieldset_add_hex(fieldset,"Flags",0x1b,6); 77 | fieldset_add_bytes(fieldset,"Unknown1",(uint8_t*)"\xff",2); 78 | fieldset_add_bytes(fieldset,"Unknown2",(uint8_t*)"\xff",2); 79 | } 80 | 81 | /* Create a Renault TPMS signal, according to the fields provided. */ 82 | static void build_message(RawSamplesBuffer *samples, ProtoViewFieldSet *fieldset) 83 | { 84 | uint32_t te = 50; // Short pulse duration in microseconds. 85 | 86 | // Preamble + sync 87 | const char *psync = "01010101010101010101010101010110"; 88 | const char *p = psync; 89 | while(*p) { 90 | raw_samples_add_or_update(samples,*p == '1',te); 91 | p++; 92 | } 93 | 94 | // Data, 9 bytes 95 | uint8_t data[9] = {0}; 96 | unsigned int raw_pressure = fieldset->fields[1]->fvalue * 4 / 3; 97 | data[0] = fieldset->fields[3]->uvalue << 2; // Flags 98 | data[0] |= (raw_pressure >> 8) & 3; // Pressure kpa high 2 bits 99 | data[1] = raw_pressure & 0xff; // Pressure kpa low 8 bits 100 | data[2] = fieldset->fields[2]->value + 30; // Temperature C 101 | memcpy(data+3,fieldset->fields[0]->bytes,6); // ID, 24 bits. 102 | data[6] = fieldset->fields[4]->bytes[0]; // Unknown 1 103 | data[7] = fieldset->fields[5]->bytes[0]; // Unknown 2 104 | data[8] = crc8(data,8,0,7); 105 | 106 | // Generate Manchester code for each bit 107 | for (uint32_t j = 0; j < 9*8; j++) { 108 | if (bitmap_get(data,sizeof(data),j)) { 109 | raw_samples_add_or_update(samples,true,te); 110 | raw_samples_add_or_update(samples,false,te); 111 | } else { 112 | raw_samples_add_or_update(samples,false,te); 113 | raw_samples_add_or_update(samples,true,te); 114 | } 115 | } 116 | } 117 | 118 | ProtoViewDecoder RenaultTPMSDecoder = { 119 | .name = "Renault TPMS", 120 | .decode = decode, 121 | .get_fields = get_fields, 122 | .build_message = build_message 123 | }; 124 | -------------------------------------------------------------------------------- /protocols/tpms/schrader.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. 3 | * 4 | * Schrader TPMS. Usually 443.92 Mhz OOK, 120us pulse len. 5 | * 6 | * 500us high pulse + Preamble + Manchester coded bits where: 7 | * 1 = 10 8 | * 0 = 01 9 | * 10 | * 60 bits of data total (first 4 nibbles is the preamble, 0xF). 11 | * 12 | * Used in FIAT-Chrysler, Mercedes, ... */ 13 | 14 | #include "../../app.h" 15 | 16 | #define USE_TEST_VECTOR 0 17 | static const char *test_vector = "000000111101010101011010010110010110101001010110100110011001100101010101011010100110100110011010101010101010101010101010101010101010101010101010"; 18 | 19 | static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) { 20 | 21 | if (USE_TEST_VECTOR) { /* Test vector to check that decoding works. */ 22 | bitmap_set_pattern(bits,numbytes,0,test_vector); 23 | numbits = strlen(test_vector); 24 | } 25 | 26 | if (numbits < 64) return false; /* Preamble + data. */ 27 | 28 | const char *sync_pattern = "1111010101" "01011010"; 29 | uint64_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern); 30 | if (off == BITMAP_SEEK_NOT_FOUND) return false; 31 | FURI_LOG_E(TAG, "Schrader TPMS gap+preamble found"); 32 | 33 | info->start_off = off; 34 | off += 10; /* Skip just the long pulse and the first 3 bits of sync, so 35 | that we have the first byte of data with the sync nibble 36 | 0011 = 0x3. */ 37 | 38 | uint8_t raw[8]; 39 | uint8_t id[4]; 40 | uint32_t decoded = 41 | convert_from_line_code(raw,sizeof(raw),bits,numbytes,off, 42 | "01","10"); /* Manchester code. */ 43 | FURI_LOG_E(TAG, "Schrader TPMS decoded bits: %lu", decoded); 44 | 45 | if (decoded < 64) return false; /* Require the full 8 bytes. */ 46 | 47 | raw[0] |= 0xf0; // Fix the preamble nibble for checksum computation. 48 | uint8_t cksum = crc8(raw,sizeof(raw)-1,0xf0,0x7); 49 | if (cksum != raw[7]) { 50 | FURI_LOG_E(TAG, "Schrader TPMS checksum mismatch"); 51 | return false; 52 | } 53 | 54 | info->pulses_count = (off+8*8*2) - info->start_off; 55 | 56 | float kpa = (float)raw[5]*2.5; 57 | int temp = raw[6]-50; 58 | id[0] = raw[1]&7; 59 | id[1] = raw[2]; 60 | id[2] = raw[3]; 61 | id[3] = raw[4]; 62 | 63 | fieldset_add_bytes(info->fieldset,"Tire ID",id,4*2); 64 | fieldset_add_float(info->fieldset,"Pressure kpa",kpa,2); 65 | fieldset_add_int(info->fieldset,"Temperature C",temp,8); 66 | return true; 67 | } 68 | 69 | ProtoViewDecoder SchraderTPMSDecoder = { 70 | .name = "Schrader TPMS", 71 | .decode = decode, 72 | .get_fields = NULL, 73 | .build_message = NULL 74 | }; 75 | -------------------------------------------------------------------------------- /protocols/tpms/schrader_eg53ma4.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. 3 | * 4 | * Schrader variant EG53MA4 TPMS. 5 | * Usually 443.92 Mhz OOK, 100us pulse len. 6 | * 7 | * Preamble: alternating pulse/gap, 100us. 8 | * Sync (as pulses and gaps): "01100101", already part of the data stream 9 | * (first nibble) corresponding to 0x4 10 | * 11 | * A total of 10 bytes payload, Manchester encoded. 12 | * 13 | * 0 = 01 14 | * 1 = 10 15 | * 16 | * Used in certain Open cars and others. 17 | */ 18 | 19 | #include "../../app.h" 20 | 21 | static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) { 22 | 23 | const char *sync_pattern = "010101010101" "01100101"; 24 | uint8_t sync_len = 12+8; /* We just use 12 preamble symbols + sync. */ 25 | if (numbits-sync_len+8 < 8*10) return false; 26 | 27 | uint64_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern); 28 | if (off == BITMAP_SEEK_NOT_FOUND) return false; 29 | FURI_LOG_E(TAG, "Schrader EG53MA4 TPMS preamble+sync found"); 30 | 31 | info->start_off = off; 32 | off += sync_len-8; /* Skip preamble, not sync that is part of the data. */ 33 | 34 | uint8_t raw[10]; 35 | uint32_t decoded = 36 | convert_from_line_code(raw,sizeof(raw),bits,numbytes,off, 37 | "01","10"); /* Manchester code. */ 38 | FURI_LOG_E(TAG, "Schrader EG53MA4 TPMS decoded bits: %lu", decoded); 39 | 40 | if (decoded < 10*8) return false; /* Require the full 10 bytes. */ 41 | 42 | /* CRC is just all bytes added mod 256. */ 43 | uint8_t crc = 0; 44 | for (int j = 0; j < 9; j++) crc += raw[j]; 45 | if (crc != raw[9]) return false; /* Require sane CRC. */ 46 | 47 | info->pulses_count = (off+10*8*2) - info->start_off; 48 | 49 | /* To convert the raw pressure to kPa, RTL433 uses 2.5, but is likely 50 | * wrong. Searching on Google for users experimenting with the value 51 | * reported, the value appears to be 2.75. */ 52 | float kpa = (float)raw[7]*2.75; 53 | int temp_f = raw[8]; 54 | int temp_c = (temp_f-32)*5/9; /* Convert Fahrenheit to Celsius. */ 55 | 56 | fieldset_add_bytes(info->fieldset,"Tire ID",raw+4,3*2); 57 | fieldset_add_float(info->fieldset,"Pressure kpa",kpa,2); 58 | fieldset_add_int(info->fieldset,"Temperature C",temp_c,8); 59 | return true; 60 | } 61 | 62 | ProtoViewDecoder SchraderEG53MA4TPMSDecoder = { 63 | .name = "Schrader EG53MA4 TPMS", 64 | .decode = decode, 65 | .get_fields = NULL, 66 | .build_message = NULL 67 | }; 68 | -------------------------------------------------------------------------------- /protocols/tpms/toyota.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. 3 | * 4 | * Toyota tires TPMS. Usually 443.92 Mhz FSK (In Europe). 5 | * 6 | * Preamble + sync + 64 bits of data. ~48us short pulse length. 7 | * 8 | * The preamble + sync is something like: 9 | * 10 | * 10101010101 (preamble) + 001111[1] (sync) 11 | * 12 | * Note: the final [1] means that sometimes it is four 1s, sometimes 13 | * five, depending on the short pulse length detection and the exact 14 | * duration of the high long pulse. After the sync, a differential 15 | * Manchester encoded payload follows. However the Flipper's CC1101 16 | * often can't decode correctly the initial alternating pattern 101010101, 17 | * so what we do is to seek just the sync, that is "001111" or "0011111", 18 | * however we now that it must be followed by one differenitally encoded 19 | * bit, so we can use also the first symbol of data to force a more robust 20 | * detection, and look for one of the following: 21 | * 22 | * [001111]00 23 | * [0011111]00 24 | * [001111]01 25 | * [0011111]01 26 | */ 27 | 28 | #include "../../app.h" 29 | 30 | static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) { 31 | 32 | if (numbits-6 < 64*2) return false; /* Ask for 64 bit of data (each bit 33 | is two symbols in the bitmap). */ 34 | 35 | char *sync[] = { 36 | "00111100", 37 | "001111100", 38 | "00111101", 39 | "001111101", 40 | NULL 41 | }; 42 | 43 | int j; 44 | uint32_t off = 0; 45 | for (j = 0; sync[j]; j++) { 46 | off = bitmap_seek_bits(bits,numbytes,0,numbits,sync[j]); 47 | if (off != BITMAP_SEEK_NOT_FOUND) { 48 | info->start_off = off; 49 | off += strlen(sync[j])-2; 50 | break; 51 | } 52 | } 53 | if (off == BITMAP_SEEK_NOT_FOUND) return false; 54 | 55 | FURI_LOG_E(TAG, "Toyota TPMS sync[%s] found", sync[j]); 56 | 57 | uint8_t raw[9]; 58 | uint32_t decoded = 59 | convert_from_diff_manchester(raw,sizeof(raw),bits,numbytes,off,true); 60 | FURI_LOG_E(TAG, "Toyota TPMS decoded bits: %lu", decoded); 61 | 62 | if (decoded < 8*9) return false; /* Require the full 8 bytes. */ 63 | if (crc8(raw,8,0x80,7) != raw[8]) return false; /* Require sane CRC. */ 64 | 65 | /* We detected a valid signal. However now info->start_off is actually 66 | * pointing to the sync part, not the preamble of alternating 0 and 1. 67 | * Protoview decoders get called with some space to the left, in order 68 | * for the decoder itself to fix the signal if neeeded, so that its 69 | * logical representation will be more accurate and better to save 70 | * and retransmit. */ 71 | if (info->start_off >= 12) { 72 | info->start_off -= 12; 73 | bitmap_set_pattern(bits,numbytes,info->start_off,"010101010101"); 74 | } 75 | 76 | info->pulses_count = (off+8*9*2) - info->start_off; 77 | 78 | float psi = (float)((raw[4]&0x7f)<<1 | raw[5]>>7) * 0.25 - 7; 79 | int temp = ((raw[5]&0x7f)<<1 | raw[6]>>7) - 40; 80 | 81 | fieldset_add_bytes(info->fieldset,"Tire ID",raw,4*2); 82 | fieldset_add_float(info->fieldset,"Pressure psi",psi,2); 83 | fieldset_add_int(info->fieldset,"Temperature C",temp,8); 84 | return true; 85 | } 86 | 87 | ProtoViewDecoder ToyotaTPMSDecoder = { 88 | .name = "Toyota TPMS", 89 | .decode = decode, 90 | .get_fields = NULL, 91 | .build_message = NULL 92 | }; 93 | -------------------------------------------------------------------------------- /protocols/unknown.c: -------------------------------------------------------------------------------- 1 | #include "../app.h" 2 | 3 | /* Copyright (C) 2023 Salvatore Sanfilippo -- All Rights Reserved 4 | * See the LICENSE file for information about the license. 5 | * 6 | * ---------------------------------------------------------------------------- 7 | * The "unknown" decoder fires as the last one, once we are sure no other 8 | * decoder was able to identify the signal. The goal is to detect the 9 | * preamble and line code used in the received signal, then turn the 10 | * decoded bits into bytes. 11 | * 12 | * The techniques used for the detection are described in the comments 13 | * below. 14 | * ---------------------------------------------------------------------------- 15 | */ 16 | 17 | 18 | /* Scan the signal bitmap looking for a PWM modulation. In this case 19 | * for PWM we are referring to two exact patterns of high and low 20 | * signal (each bit in the bitmap is worth the smallest gap/pulse duration 21 | * we detected) that repeat each other in a given segment of the message. 22 | * 23 | * This modulation is quite common, for instance sometimes zero and 24 | * one are rappresented by a 700us pulse followed by 350 gap, 25 | * and 350us pulse followed by a 700us gap. So the signal bitmap received 26 | * by the decoder would contain 110 and 100 symbols. 27 | * 28 | * The way this function work is commented inline. 29 | * 30 | * The function returns the number of consecutive symbols found, having 31 | * a symbol length of 'symlen' (3 in the above example), and stores 32 | * in *s1i the offset of the first symbol found, and in *s2i the offset 33 | * of the second symbol. The function can't tell which is one and which 34 | * zero. */ 35 | static uint32_t find_pwm(uint8_t *bits, uint32_t numbytes, uint32_t numbits, 36 | uint32_t symlen, uint32_t *s1i, uint32_t *s2i) 37 | { 38 | uint32_t best_count = 0; /* Max number of symbols found in this try. */ 39 | uint32_t best_idx1 = 0; /* First symbol offset of longest sequence found. 40 | * This is also the start sequence offset. */ 41 | uint32_t best_idx2 = 0; /* Second symbol offset. */ 42 | 43 | /* Try all the possible symbol offsets that are less of our 44 | * symbol len. This is likely not really useful but we take 45 | * a conservative approach. Because if have have, for instance, 46 | * repeating symbols "100" and "110", they will form a sequence 47 | * that is choerent at different offsets, but out-of-sync. 48 | * 49 | * Anyway at the end of the function we try to fix the sync. */ 50 | for (uint32_t off = 0; off < symlen; off++) { 51 | uint32_t c = 0; // Number of contiguous symbols found. 52 | uint32_t c1 = 0, c2 = 0; // Occurrences of first/second symbol. 53 | *s1i = off; // Assume we start at one symbol boundaty. 54 | *s2i = UINT32_MAX; // Second symbol first index still unknown. 55 | uint32_t next = off; 56 | 57 | /* We scan the whole bitmap in one pass, resetting the state 58 | * each time we find a pattern that is not one of the two 59 | * symbols we found so far. */ 60 | while(next < numbits-symlen) { 61 | bool match1 = bitmap_match_bitmap(bits,numbytes,next, 62 | bits,numbytes,*s1i, 63 | symlen); 64 | if (!match1 && *s2i == UINT32_MAX) { 65 | /* It's not the first sybol. We don't know how the 66 | * second look like. Assume we found an occurrence of 67 | * the second symbol. */ 68 | *s2i = next; 69 | } 70 | 71 | bool match2 = bitmap_match_bitmap(bits,numbytes,next, 72 | bits,numbytes,*s2i, 73 | symlen); 74 | 75 | /* One or the other should match. */ 76 | if (match1 || match2) { 77 | c++; 78 | if (match1) c1++; 79 | if (match2) c2++; 80 | if (c > best_count && 81 | c1 >= best_count/5 && // Require enough presence of both 82 | c2 >= best_count/5) // zero and one. 83 | { 84 | best_count = c; 85 | best_idx1 = *s1i; 86 | best_idx2 = *s2i; 87 | } 88 | next += symlen; 89 | } else { 90 | /* No match. Continue resetting the signal info. */ 91 | c = 0; // Start again to count contiguous symbols. 92 | c1 = 0; 93 | c2 = 0; 94 | *s1i = next; // First symbol always at start. 95 | *s2i = UINT32_MAX; // Second symbol unknown. 96 | } 97 | } 98 | } 99 | 100 | /* We don't know if we are really synchronized with the bits at this point. 101 | * For example if zero bit is 100 and one bit is 110 in a specific 102 | * line code, our detector could randomly believe it's 001 and 101. 103 | * However PWD line codes normally start with a pulse in both symbols. 104 | * If that is the case, let's align. */ 105 | uint32_t shift; 106 | for (shift = 0; shift < symlen; shift++) { 107 | if (bitmap_get(bits,numbytes,best_idx1+shift) && 108 | bitmap_get(bits,numbytes,best_idx2+shift)) break; 109 | } 110 | if (shift != symlen) { 111 | best_idx1 += shift; 112 | best_idx2 += shift; 113 | } 114 | 115 | *s1i = best_idx1; 116 | *s2i = best_idx2; 117 | return best_count; 118 | } 119 | 120 | /* Find the longest sequence that looks like Manchester coding. 121 | * 122 | * Manchester coding requires each pairs of bits to be either 123 | * 01 or 10. We'll have to try odd and even offsets to be 124 | * sure to find it. 125 | * 126 | * Note that this will also detect differential Manchester, but 127 | * will report it as Manchester. I can't think of any way to 128 | * distinguish between the two line codes, because shifting them 129 | * one symbol will make one to look like the other. 130 | * 131 | * Only option could be to decode the message with both line 132 | * codes and use statistical properties (common byte values) 133 | * to determine what's more likely, but this looks very fragile. 134 | * 135 | * Fortunately differential Manchester is more rarely used, 136 | * so we can assume Manchester most of the times. Yet we are left 137 | * with the indetermination about zero being pulse-gap or gap-pulse 138 | * or the other way around. 139 | * 140 | * If the 'only_raising' parameter is true, the function detects 141 | * only sequences going from gap to pulse: this is useful in order 142 | * to locate preambles of alternating gaps and pulses. */ 143 | static uint32_t find_alternating_bits(uint8_t *bits, uint32_t numbytes, 144 | uint32_t numbits, uint32_t *start, bool only_raising) 145 | { 146 | uint32_t best_count = 0; // Max number of symbols found 147 | uint32_t best_off = 0; // Max symbols start offset. 148 | for (int odd = 0; odd < 2; odd++) { 149 | uint32_t count = 0; // Symbols found so far 150 | uint32_t start_off = odd; 151 | uint32_t j = odd; 152 | while (j < numbits-1) { 153 | bool bit1 = bitmap_get(bits,numbytes,j); 154 | bool bit2 = bitmap_get(bits,numbytes,j+1); 155 | if ((!only_raising && bit1 != bit2) || 156 | (only_raising && !bit1 && bit2)) 157 | { 158 | count++; 159 | if (count > best_count) { 160 | best_count = count; 161 | best_off = start_off; 162 | } 163 | } else { 164 | /* End of sequence. Continue with the next 165 | * part of the signal. */ 166 | count = 0; 167 | start_off = j + 2; 168 | } 169 | j += 2; 170 | } 171 | } 172 | *start = best_off; 173 | return best_count; 174 | } 175 | 176 | /* Wrapper to find Manchester code. */ 177 | static uint32_t find_manchester(uint8_t *bits, uint32_t numbytes, 178 | uint32_t numbits, uint32_t *start) 179 | { 180 | return find_alternating_bits(bits,numbytes,numbits,start,false); 181 | } 182 | 183 | /* Wrapper to find preamble sections. */ 184 | static uint32_t find_preamble(uint8_t *bits, uint32_t numbytes, 185 | uint32_t numbits, uint32_t *start) 186 | { 187 | return find_alternating_bits(bits,numbytes,numbits,start,true); 188 | } 189 | 190 | typedef enum { 191 | LineCodeNone, 192 | LineCodeManchester, 193 | LineCodePWM3, 194 | LineCodePWM4, 195 | } LineCodeGuess; 196 | 197 | static char *get_linecode_name(LineCodeGuess lc) { 198 | switch(lc) { 199 | case LineCodeNone: return "none"; 200 | case LineCodeManchester: return "Manchester"; 201 | case LineCodePWM3: return "PWM3"; 202 | case LineCodePWM4: return "PWM4"; 203 | } 204 | return "unknown"; 205 | } 206 | 207 | static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) { 208 | 209 | /* No decoder was able to detect this message. Let's try if we can 210 | * find some structure. To start, we'll see if it looks like is 211 | * manchester coded, or PWM with symbol len of 3 or 4. */ 212 | 213 | /* For PWM, start1 and start2 are the offsets at which the two 214 | * sequences composing the message appear the first time. 215 | * So start1 is also the message start offset. Start2 is not used 216 | * for Manchester, that does not have two separated symbols like 217 | * PWM. */ 218 | uint32_t start1 = 0, start2 = 0; 219 | uint32_t msgbits; // Number of message bits in the bitmap, so 220 | // this will be the number of symbols, not actual 221 | // bits after the message is decoded. 222 | uint32_t tmp1, tmp2; // Temp vars to store the start. 223 | uint32_t minbits = 16; // Less than that gets undetected. 224 | uint32_t pwm_len; // Bits per symbol, in the case of PWM. 225 | LineCodeGuess linecode = LineCodeNone; 226 | 227 | // Try PWM3 228 | uint32_t pwm3_bits = find_pwm(bits,numbytes,numbits,3,&tmp1,&tmp2); 229 | if (pwm3_bits >= minbits) { 230 | linecode = LineCodePWM3; 231 | start1 = tmp1; 232 | start2 = tmp2; 233 | pwm_len = 3; 234 | msgbits = pwm3_bits*pwm_len; 235 | } 236 | 237 | // Try PWM4 238 | uint32_t pwm4_bits = find_pwm(bits,numbytes,numbits,4,&tmp1,&tmp2); 239 | if (pwm4_bits >= minbits && pwm4_bits > pwm3_bits) { 240 | linecode = LineCodePWM4; 241 | start1 = tmp1; 242 | start2 = tmp2; 243 | pwm_len = 4; 244 | msgbits = pwm3_bits*pwm_len; 245 | } 246 | 247 | // Try Manchester 248 | uint32_t manchester_bits = find_manchester(bits,numbytes,numbits,&tmp1); 249 | if (manchester_bits > minbits && 250 | manchester_bits > pwm3_bits && 251 | manchester_bits > pwm4_bits) 252 | { 253 | linecode = LineCodeManchester; 254 | start1 = tmp1; 255 | msgbits = manchester_bits*2; 256 | FURI_LOG_E(TAG, "MANCHESTER START: %lu", tmp1); 257 | } 258 | 259 | if (linecode == LineCodeNone) return false; 260 | 261 | /* Often there is a preamble before the signal. We'll try to find 262 | * it, and if it is not too far away from our signal, we'll claim 263 | * our signal starts at the preamble. */ 264 | uint32_t preamble_len = find_preamble(bits,numbytes,numbits,&tmp1); 265 | uint32_t min_preamble_len = 10; 266 | uint32_t max_preamble_distance = 32; 267 | uint32_t preamble_start = 0; 268 | bool preamble_found = false; 269 | 270 | /* Note that because of the following checks, if the Manchester detector 271 | * detected the preamble bits as data, we are ok with that, since it 272 | * means that the synchronization is not designed to "break" the bits 273 | * flow. In this case we ignore the preamble and return all as data. */ 274 | if (preamble_len >= min_preamble_len && // Not too short. 275 | tmp1 < start1 && // Should be before the data. 276 | start1-tmp1 <= max_preamble_distance) // Not too far. 277 | { 278 | preamble_start = tmp1; 279 | preamble_found = true; 280 | } 281 | 282 | info->start_off = preamble_found ? preamble_start : start1; 283 | info->pulses_count = (start1+msgbits) - info->start_off; 284 | info->pulses_count += 20; /* Add a few more, so that if the user resends 285 | * the message, it is more likely we will 286 | * transfer all that is needed, like a message 287 | * terminator (that we don't detect). */ 288 | 289 | if (preamble_found) 290 | FURI_LOG_E(TAG, "PREAMBLE AT: %lu", preamble_start); 291 | FURI_LOG_E(TAG, "START: %lu", info->start_off); 292 | FURI_LOG_E(TAG, "MSGBITS: %lu", msgbits); 293 | FURI_LOG_E(TAG, "DATASTART: %lu", start1); 294 | FURI_LOG_E(TAG, "PULSES: %lu", info->pulses_count); 295 | 296 | /* We think there is a message and we know where it starts and the 297 | * line code used. We can turn it into bits and bytes. */ 298 | uint32_t decoded; 299 | uint8_t data[32]; 300 | uint32_t datalen; 301 | 302 | char symbol1[5], symbol2[5]; 303 | if (linecode == LineCodePWM3 || linecode == LineCodePWM4) { 304 | bitmap_to_string(symbol1,bits,numbytes,start1,pwm_len); 305 | bitmap_to_string(symbol2,bits,numbytes,start2,pwm_len); 306 | } else if (linecode == LineCodeManchester) { 307 | memcpy(symbol1,"01",3); 308 | memcpy(symbol2,"10",3); 309 | } 310 | 311 | decoded = convert_from_line_code(data,sizeof(data),bits,numbytes,start1, 312 | symbol1,symbol2); 313 | datalen = (decoded+7)/8; 314 | 315 | char *linecode_name = get_linecode_name(linecode); 316 | fieldset_add_str(info->fieldset,"line code", 317 | linecode_name,strlen(linecode_name)); 318 | fieldset_add_uint(info->fieldset,"data bits",decoded,8); 319 | if (preamble_found) 320 | fieldset_add_uint(info->fieldset,"preamble len",preamble_len,8); 321 | fieldset_add_str(info->fieldset,"first symbol",symbol1,strlen(symbol1)); 322 | fieldset_add_str(info->fieldset,"second symbol",symbol2,strlen(symbol2)); 323 | for (uint32_t j = 0; j < datalen; j++) { 324 | char label[16]; 325 | snprintf(label,sizeof(label),"data[%lu]",j); 326 | fieldset_add_bytes(info->fieldset,label,data+j,2); 327 | } 328 | return true; 329 | } 330 | 331 | ProtoViewDecoder UnknownDecoder = { 332 | .name = "Unknown", 333 | .decode = decode, 334 | .get_fields = NULL, 335 | .build_message = NULL 336 | }; 337 | -------------------------------------------------------------------------------- /raw_samples.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "raw_samples.h" 9 | 10 | /* Allocate and initialize a samples buffer. */ 11 | RawSamplesBuffer *raw_samples_alloc(void) { 12 | RawSamplesBuffer *buf = malloc(sizeof(*buf)); 13 | buf->mutex = furi_mutex_alloc(FuriMutexTypeNormal); 14 | raw_samples_reset(buf); 15 | return buf; 16 | } 17 | 18 | /* Free a sample buffer. Should be called when the mutex is released. */ 19 | void raw_samples_free(RawSamplesBuffer *s) { 20 | furi_mutex_free(s->mutex); 21 | free(s); 22 | } 23 | 24 | /* This just set all the samples to zero and also resets the internal 25 | * index. There is no need to call it after raw_samples_alloc(), but only 26 | * when one wants to reset the whole buffer of samples. */ 27 | void raw_samples_reset(RawSamplesBuffer *s) { 28 | furi_mutex_acquire(s->mutex,FuriWaitForever); 29 | s->total = RAW_SAMPLES_NUM; 30 | s->idx = 0; 31 | s->short_pulse_dur = 0; 32 | memset(s->samples,0,sizeof(s->samples)); 33 | furi_mutex_release(s->mutex); 34 | } 35 | 36 | /* Set the raw sample internal index so that what is currently at 37 | * offset 'offset', will appear to be at 0 index. */ 38 | void raw_samples_center(RawSamplesBuffer *s, uint32_t offset) { 39 | s->idx = (s->idx+offset) % RAW_SAMPLES_NUM; 40 | } 41 | 42 | /* Add the specified sample in the circular buffer. */ 43 | void raw_samples_add(RawSamplesBuffer *s, bool level, uint32_t dur) { 44 | furi_mutex_acquire(s->mutex,FuriWaitForever); 45 | s->samples[s->idx].level = level; 46 | s->samples[s->idx].dur = dur; 47 | s->idx = (s->idx+1) % RAW_SAMPLES_NUM; 48 | furi_mutex_release(s->mutex); 49 | } 50 | 51 | /* This is like raw_samples_add(), however in case a sample of the 52 | * same level of the previous one is added, the duration of the last 53 | * sample is updated instead. Needed mainly for the decoders build_message() 54 | * methods: it is simpler to write an encoder of a signal like that, 55 | * just creating messages piece by piece. 56 | * 57 | * This function is a bit slower so the internal data sampling should 58 | * be performed with raw_samples_add(). */ 59 | void raw_samples_add_or_update(RawSamplesBuffer *s, bool level, uint32_t dur) { 60 | furi_mutex_acquire(s->mutex,FuriWaitForever); 61 | uint32_t previdx = (s->idx-1) % RAW_SAMPLES_NUM; 62 | if (s->samples[previdx].level == level && 63 | s->samples[previdx].dur != 0) 64 | { 65 | /* Update the last sample: it has the same level. */ 66 | s->samples[previdx].dur += dur; 67 | } else { 68 | /* Add a new sample. */ 69 | s->samples[s->idx].level = level; 70 | s->samples[s->idx].dur = dur; 71 | s->idx = (s->idx+1) % RAW_SAMPLES_NUM; 72 | } 73 | furi_mutex_release(s->mutex); 74 | } 75 | 76 | /* Get the sample from the buffer. It is possible to use out of range indexes 77 | * as 'idx' because the modulo operation will rewind back from the start. */ 78 | void raw_samples_get(RawSamplesBuffer *s, uint32_t idx, bool *level, uint32_t *dur) 79 | { 80 | furi_mutex_acquire(s->mutex,FuriWaitForever); 81 | idx = (s->idx + idx) % RAW_SAMPLES_NUM; 82 | *level = s->samples[idx].level; 83 | *dur = s->samples[idx].dur; 84 | furi_mutex_release(s->mutex); 85 | } 86 | 87 | /* Copy one buffer to the other, including current index. */ 88 | void raw_samples_copy(RawSamplesBuffer *dst, RawSamplesBuffer *src) { 89 | furi_mutex_acquire(src->mutex,FuriWaitForever); 90 | furi_mutex_acquire(dst->mutex,FuriWaitForever); 91 | dst->idx = src->idx; 92 | dst->short_pulse_dur = src->short_pulse_dur; 93 | memcpy(dst->samples,src->samples,sizeof(dst->samples)); 94 | furi_mutex_release(src->mutex); 95 | furi_mutex_release(dst->mutex); 96 | } 97 | -------------------------------------------------------------------------------- /raw_samples.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | /* Our circular buffer of raw samples, used in order to display 5 | * the signal. */ 6 | 7 | #define RAW_SAMPLES_NUM 2048 /* Use a power of two: we take the modulo 8 | of the index quite often to normalize inside 9 | the range, and division is slow. */ 10 | typedef struct RawSamplesBuffer { 11 | FuriMutex *mutex; 12 | struct { 13 | uint16_t level:1; 14 | uint16_t dur:15; 15 | } samples[RAW_SAMPLES_NUM]; 16 | uint32_t idx; /* Current idx (next to write). */ 17 | uint32_t total; /* Total samples: same as RAW_SAMPLES_NUM, we provide 18 | this field for a cleaner interface with the user, but 19 | we always use RAW_SAMPLES_NUM when taking the modulo so 20 | the compiler can optimize % as bit masking. */ 21 | /* Signal features. */ 22 | uint32_t short_pulse_dur; /* Duration of the shortest pulse. */ 23 | } RawSamplesBuffer; 24 | 25 | RawSamplesBuffer *raw_samples_alloc(void); 26 | void raw_samples_reset(RawSamplesBuffer *s); 27 | void raw_samples_center(RawSamplesBuffer *s, uint32_t offset); 28 | void raw_samples_add(RawSamplesBuffer *s, bool level, uint32_t dur); 29 | void raw_samples_add_or_update(RawSamplesBuffer *s, bool level, uint32_t dur); 30 | void raw_samples_get(RawSamplesBuffer *s, uint32_t idx, bool *level, uint32_t *dur); 31 | void raw_samples_copy(RawSamplesBuffer *dst, RawSamplesBuffer *src); 32 | void raw_samples_free(RawSamplesBuffer *s); 33 | -------------------------------------------------------------------------------- /signal_file.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * Copyright (C) 2023 Maciej Wojtasik -- All Rights Reserved 3 | * See the LICENSE file for information about the license. */ 4 | 5 | #include "app.h" 6 | #include 7 | #include 8 | 9 | /* ========================= Signal file operations ========================= */ 10 | 11 | /* This function saves the current logical signal on disk. What is saved here 12 | * is not the signal as level and duration as we received it from CC1101, 13 | * but it's logical representation stored in the app->msg_info bitmap, where 14 | * each 1 or 0 means a puls or gap for the specified short pulse duration time 15 | * (te). */ 16 | bool save_signal(ProtoViewApp *app, const char *filename) { 17 | /* We have a message at all? */ 18 | if (app->msg_info == NULL || app->msg_info->pulses_count == 0) return false; 19 | 20 | Storage *storage = furi_record_open(RECORD_STORAGE); 21 | FlipperFormat *file = flipper_format_file_alloc(storage); 22 | Stream *stream = flipper_format_get_raw_stream(file); 23 | FuriString *file_content = NULL; 24 | bool success = true; 25 | 26 | if (flipper_format_file_open_always(file, filename)) { 27 | /* Write the file header. */ 28 | FuriString *file_content = furi_string_alloc(); 29 | const char *preset_id = ProtoViewModulations[app->modulation].id; 30 | 31 | furi_string_printf(file_content, 32 | "Filetype: Flipper SubGhz RAW File\n" 33 | "Version: 1\n" 34 | "Frequency: %ld\n" 35 | "Preset: %s\n", 36 | app->frequency, 37 | preset_id ? preset_id : "FuriHalSubGhzPresetCustom"); 38 | 39 | /* For custom modulations, we need to emit a set of registers. */ 40 | if (preset_id == NULL) { 41 | FuriString *custom = furi_string_alloc(); 42 | uint8_t *regs = ProtoViewModulations[app->modulation].custom; 43 | furi_string_printf(custom, 44 | "Custom_preset_module: CC1101\n" 45 | "Custom_preset_data: "); 46 | 47 | /* We will know the size of the preset data once we reach the end 48 | * of the registers (null address). For now it's INT_MAX. */ 49 | int preset_data_size = INT_MAX; 50 | bool patable_reached = false; 51 | for(int j = 0; j <= preset_data_size; j += 2) { 52 | // End reached, set the size to write the remaining 8 bytes (PATABLE) 53 | if (!patable_reached && regs[j] == 0) { 54 | preset_data_size = j + 8; 55 | patable_reached = true; 56 | } 57 | furi_string_cat_printf(custom, "%02X %02X ", 58 | (int)regs[j], (int)regs[j+1]); 59 | } 60 | size_t len = furi_string_size(custom); 61 | furi_string_set_char(custom,len-1,'\n'); 62 | furi_string_cat(file_content,custom); 63 | furi_string_free(custom); 64 | } 65 | 66 | /* We always save raw files. */ 67 | furi_string_cat_printf(file_content, 68 | "Protocol: RAW\n" 69 | "RAW_Data: -10000\n"); // Start with 10 ms of gap 70 | 71 | /* Write header. */ 72 | size_t len = furi_string_size(file_content); 73 | if (stream_write(stream, 74 | (uint8_t*) furi_string_get_cstr(file_content), len) 75 | != len) 76 | { 77 | FURI_LOG_W(TAG, "Short write to file"); 78 | success = false; 79 | goto write_err; 80 | } 81 | furi_string_reset(file_content); 82 | 83 | /* Write raw data sections. The Flipper subghz parser can't handle 84 | * too much data on a single line, so we generate a new one 85 | * every few samples. */ 86 | uint32_t this_line_samples = 0; 87 | uint32_t max_line_samples = 100; 88 | uint32_t idx = 0; // Iindex in the signal bitmap. 89 | ProtoViewMsgInfo *i = app->msg_info; 90 | while(idx < i->pulses_count) { 91 | bool level = bitmap_get(i->bits,i->bits_bytes,idx); 92 | uint32_t te_times = 1; 93 | idx++; 94 | /* Count the duration of the current pulse/gap. */ 95 | while(idx < i->pulses_count && 96 | bitmap_get(i->bits,i->bits_bytes,idx) == level) 97 | { 98 | te_times++; 99 | idx++; 100 | } 101 | // Invariant: after the loop 'idx' is at the start of the 102 | // next gap or pulse. 103 | 104 | int32_t dur = (int32_t)i->short_pulse_dur * te_times; 105 | if (level == 0) dur = -dur; /* Negative is gap in raw files. */ 106 | 107 | /* Emit the sample. If this is the first sample of the line, 108 | * also emit the RAW_Data: field. */ 109 | if (this_line_samples == 0) 110 | furi_string_cat_printf(file_content,"RAW_Data: "); 111 | furi_string_cat_printf(file_content,"%d ",(int)dur); 112 | this_line_samples++; 113 | 114 | /* Store the current set of samples on disk, when we reach a 115 | * given number or the end of the signal. */ 116 | bool end_reached = (idx == i->pulses_count); 117 | if (this_line_samples == max_line_samples || end_reached) { 118 | /* If that's the end, terminate the signal with a long 119 | * gap. */ 120 | if (end_reached) furi_string_cat_printf(file_content,"-10000 "); 121 | 122 | /* We always have a trailing space in the last sample. Make it 123 | * a newline. */ 124 | size_t len = furi_string_size(file_content); 125 | furi_string_set_char(file_content,len-1,'\n'); 126 | 127 | if (stream_write(stream, 128 | (uint8_t*) furi_string_get_cstr(file_content), 129 | len) != len) 130 | { 131 | FURI_LOG_W(TAG, "Short write to file"); 132 | success = false; 133 | goto write_err; 134 | } 135 | 136 | /* Prepare for next line. */ 137 | furi_string_reset(file_content); 138 | this_line_samples = 0; 139 | } 140 | } 141 | } else { 142 | success = false; 143 | FURI_LOG_W(TAG, "Unable to open file"); 144 | } 145 | 146 | write_err: 147 | furi_record_close(RECORD_STORAGE); 148 | flipper_format_free(file); 149 | if (file_content != NULL) furi_string_free(file_content); 150 | return success; 151 | } 152 | -------------------------------------------------------------------------------- /ui.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | #include "app.h" 5 | 6 | /* =========================== Subview handling ================================ 7 | * Note that these are not the Flipper subviews, but the subview system 8 | * implemented inside ProtoView. 9 | * ========================================================================== */ 10 | 11 | /* Return the ID of the currently selected subview, of the current 12 | * view. */ 13 | int ui_get_current_subview(ProtoViewApp *app) { 14 | return app->current_subview[app->current_view]; 15 | } 16 | 17 | /* Called by view rendering callback that has subviews, to show small triangles 18 | * facing down/up if there are other subviews the user can access with up 19 | * and down. */ 20 | void ui_show_available_subviews(Canvas *canvas, ProtoViewApp *app, 21 | int last_subview) 22 | { 23 | int subview = ui_get_current_subview(app); 24 | if (subview != 0) 25 | canvas_draw_triangle(canvas,120,5,8,5,CanvasDirectionBottomToTop); 26 | if (subview != last_subview-1) 27 | canvas_draw_triangle(canvas,120,59,8,5,CanvasDirectionTopToBottom); 28 | } 29 | 30 | /* Handle up/down keys when we are in a subview. If the function catched 31 | * such keypress, it returns true, so that the actual view input callback 32 | * knows it can just return ASAP without doing anything. */ 33 | bool ui_process_subview_updown(ProtoViewApp *app, InputEvent input, int last_subview) { 34 | int subview = ui_get_current_subview(app); 35 | if (input.type == InputTypePress) { 36 | if (input.key == InputKeyUp) { 37 | if (subview != 0) 38 | app->current_subview[app->current_view]--; 39 | return true; 40 | } else if (input.key == InputKeyDown) { 41 | if (subview != last_subview-1) 42 | app->current_subview[app->current_view]++; 43 | return true; 44 | } 45 | } 46 | return false; 47 | } 48 | 49 | /* ============================= Text input ==================================== 50 | * Normally we just use our own private UI widgets. However for the text input 51 | * widget, that is quite complex, visualizes a keyboard and must be standardized 52 | * for user coherent experience, we use the one provided by the Flipper 53 | * framework. The following two functions allow to show the keyboard to get 54 | * text and later dismiss it. 55 | * ========================================================================== */ 56 | 57 | /* Show the keyboard, take the user input and store it into the specified 58 | * 'buffer' of 'buflen' total bytes. When the user is done, the done_callback 59 | * is called passing the application context to it. Such callback needs 60 | * to do whatever it wants with the input buffer and dismissi the keyboard 61 | * calling: dismiss_keyboard(app); 62 | * 63 | * Note: if the buffer is not a null-termined zero string, what it contains will 64 | * be used as initial input for the user. */ 65 | void ui_show_keyboard(ProtoViewApp *app, char *buffer, uint32_t buflen, 66 | void (*done_callback)(void*)) 67 | { 68 | app->show_text_input = true; 69 | app->text_input_buffer = buffer; 70 | app->text_input_buffer_len = buflen; 71 | app->text_input_done_callback = done_callback; 72 | } 73 | 74 | void ui_dismiss_keyboard(ProtoViewApp *app) { 75 | view_dispatcher_stop(app->view_dispatcher); 76 | } 77 | 78 | /* ================================= Alert ================================== */ 79 | 80 | /* Set an alert message to be shown over any currently active view, for 81 | * the specified amount of time of 'ttl' milliseconds. */ 82 | void ui_show_alert(ProtoViewApp *app, const char *text, uint32_t ttl) { 83 | app->alert_dismiss_time = furi_get_tick() + furi_ms_to_ticks(ttl); 84 | snprintf(app->alert_text,ALERT_MAX_LEN,"%s",text); 85 | } 86 | 87 | /* Cancel the alert before its time has elapsed. */ 88 | void ui_dismiss_alert(ProtoViewApp *app) { 89 | app->alert_dismiss_time = 0; 90 | } 91 | 92 | /* Show the alert if an alert is set. This is called after the currently 93 | * active view displayed its stuff, so we overwrite the screen with the 94 | * alert message. */ 95 | void ui_draw_alert_if_needed(Canvas *canvas, ProtoViewApp *app) { 96 | if (app->alert_dismiss_time == 0) { 97 | /* No active alert. */ 98 | return; 99 | } else if (app->alert_dismiss_time < furi_get_tick()) { 100 | /* Alert just expired. */ 101 | ui_dismiss_alert(app); 102 | return; 103 | } 104 | 105 | /* Show the alert. A box with black border and a text inside. */ 106 | canvas_set_font(canvas, FontPrimary); 107 | uint8_t w = canvas_string_width(canvas, app->alert_text); 108 | uint8_t h = 8; // Font height. 109 | uint8_t text_x = 64-(w/2); 110 | uint8_t text_y = 32+4; 111 | uint8_t padding = 3; 112 | canvas_set_color(canvas,ColorBlack); 113 | canvas_draw_box(canvas,text_x-padding,text_y-padding-h,w+padding*2,h+padding*2); 114 | canvas_set_color(canvas,ColorWhite); 115 | canvas_draw_box(canvas,text_x-padding+1,text_y-padding-h+1,w+padding*2-2,h+padding*2-2); 116 | canvas_set_color(canvas,ColorBlack); 117 | canvas_draw_str(canvas,text_x,text_y,app->alert_text); 118 | } 119 | 120 | /* =========================== Canvas extensions ============================ */ 121 | 122 | void canvas_draw_str_with_border(Canvas* canvas, uint8_t x, uint8_t y, const char* str, Color text_color, Color border_color) 123 | { 124 | struct { 125 | uint8_t x; uint8_t y; 126 | } dir[8] = { 127 | {-1,-1}, 128 | {0,-1}, 129 | {1,-1}, 130 | {1,0}, 131 | {1,1}, 132 | {0,1}, 133 | {-1,1}, 134 | {-1,0} 135 | }; 136 | 137 | /* Rotate in all the directions writing the same string to create a 138 | * border, then write the actual string in the other color in the 139 | * middle. */ 140 | canvas_set_color(canvas, border_color); 141 | for (int j = 0; j < 8; j++) 142 | canvas_draw_str(canvas,x+dir[j].x,y+dir[j].y,str); 143 | canvas_set_color(canvas, text_color); 144 | canvas_draw_str(canvas,x,y,str); 145 | canvas_set_color(canvas, ColorBlack); 146 | } 147 | -------------------------------------------------------------------------------- /view_build.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | #include "app.h" 5 | 6 | extern ProtoViewDecoder *Decoders[]; // Defined in signal.c. 7 | 8 | /* Our view private data. */ 9 | #define USER_VALUE_LEN 64 10 | typedef struct { 11 | ProtoViewDecoder *decoder; /* Decoder we are using to create a 12 | message. */ 13 | uint32_t cur_decoder; /* Decoder index when we are yet selecting 14 | a decoder. Used when decoder is NULL. */ 15 | ProtoViewFieldSet *fieldset; /* The fields to populate. */ 16 | uint32_t cur_field; /* Field we are editing right now. This 17 | is the index inside the 'fieldset' 18 | fields. */ 19 | char *user_value; /* Keyboard input to replace the current 20 | field value goes here. */ 21 | } BuildViewPrivData; 22 | 23 | /* Not all the decoders support message bulding, so we can't just 24 | * increment / decrement the cur_decoder index here. */ 25 | static void select_next_decoder(ProtoViewApp *app) { 26 | BuildViewPrivData *privdata = app->view_privdata; 27 | do { 28 | privdata->cur_decoder++; 29 | if (Decoders[privdata->cur_decoder] == NULL) 30 | privdata->cur_decoder = 0; 31 | } while(Decoders[privdata->cur_decoder]->get_fields == NULL); 32 | } 33 | 34 | /* Like select_next_decoder() but goes backward. */ 35 | static void select_prev_decoder(ProtoViewApp *app) { 36 | BuildViewPrivData *privdata = app->view_privdata; 37 | do { 38 | if (privdata->cur_decoder == 0) { 39 | /* Go one after the last one to wrap around. */ 40 | while(Decoders[privdata->cur_decoder]) privdata->cur_decoder++; 41 | } 42 | privdata->cur_decoder--; 43 | } while(Decoders[privdata->cur_decoder]->get_fields == NULL); 44 | } 45 | 46 | /* Render the view to select the decoder, among the ones that 47 | * support message building. */ 48 | static void render_view_select_decoder(Canvas *const canvas, ProtoViewApp *app) { 49 | BuildViewPrivData *privdata = app->view_privdata; 50 | canvas_set_font(canvas, FontPrimary); 51 | canvas_draw_str(canvas, 0, 9, "Signal creator"); 52 | canvas_set_font(canvas, FontSecondary); 53 | canvas_draw_str(canvas, 0, 19, "up/down: select, ok: choose"); 54 | 55 | canvas_set_font(canvas, FontPrimary); 56 | canvas_draw_str_aligned(canvas,64,38,AlignCenter,AlignCenter, 57 | Decoders[privdata->cur_decoder]->name); 58 | } 59 | 60 | /* Render the view that allows the user to populate the fields needed 61 | * for the selected decoder to build a message. */ 62 | static void render_view_set_fields(Canvas *const canvas, ProtoViewApp *app) { 63 | BuildViewPrivData *privdata = app->view_privdata; 64 | char buf[32]; 65 | snprintf(buf,sizeof(buf), "%s field %d/%d", 66 | privdata->decoder->name, (int)privdata->cur_field+1, 67 | (int)privdata->fieldset->numfields); 68 | canvas_set_color(canvas,ColorBlack); 69 | canvas_draw_box(canvas,0,0,128,21); 70 | canvas_set_color(canvas,ColorWhite); 71 | canvas_set_font(canvas, FontPrimary); 72 | canvas_draw_str(canvas, 1, 9, buf); 73 | canvas_set_font(canvas, FontSecondary); 74 | canvas_draw_str(canvas, 1, 19, "up/down: next field, ok: edit"); 75 | 76 | /* Write the field name, type, current content. */ 77 | canvas_set_color(canvas,ColorBlack); 78 | ProtoViewField *field = privdata->fieldset->fields[privdata->cur_field]; 79 | snprintf(buf,sizeof(buf), "%s %s:%d", field->name, 80 | field_get_type_name(field), (int)field->len); 81 | buf[0] = toupper(buf[0]); 82 | canvas_set_font(canvas, FontPrimary); 83 | canvas_draw_str_aligned(canvas,64,30,AlignCenter,AlignCenter,buf); 84 | canvas_set_font(canvas, FontSecondary); 85 | 86 | /* Render the current value between "" */ 87 | unsigned int written = (unsigned int) field_to_string(buf+1,sizeof(buf)-1,field); 88 | buf[0] = '"'; 89 | if (written+3 < sizeof(buf)) memcpy(buf+written+1,"\"\x00",2); 90 | canvas_draw_str_aligned(canvas,63,45,AlignCenter,AlignCenter,buf); 91 | 92 | /* Footer instructions. */ 93 | canvas_draw_str(canvas, 0, 62, "Long ok: create, < > incr/decr"); 94 | } 95 | 96 | /* Render the build message view. */ 97 | void render_view_build_message(Canvas *const canvas, ProtoViewApp *app) { 98 | BuildViewPrivData *privdata = app->view_privdata; 99 | 100 | if (privdata->decoder) 101 | render_view_set_fields(canvas,app); 102 | else 103 | render_view_select_decoder(canvas,app); 104 | } 105 | 106 | /* Handle input for the decoder selection. */ 107 | static void process_input_select_decoder(ProtoViewApp *app, InputEvent input) { 108 | BuildViewPrivData *privdata = app->view_privdata; 109 | if (input.type == InputTypeShort) { 110 | if (input.key == InputKeyOk) { 111 | privdata->decoder = Decoders[privdata->cur_decoder]; 112 | privdata->fieldset = fieldset_new(); 113 | privdata->decoder->get_fields(privdata->fieldset); 114 | 115 | /* If the currently decoded message was produced with the 116 | * same decoder the user selected, let's populate the 117 | * defaults with the current values. So the user will 118 | * actaully edit the current message. */ 119 | if (app->signal_decoded && 120 | app->msg_info->decoder == privdata->decoder) 121 | { 122 | fieldset_copy_matching_fields(privdata->fieldset, 123 | app->msg_info->fieldset); 124 | } 125 | 126 | /* Now we use the subview system in order to protect the 127 | message editing mode from accidental < or > presses. 128 | Since we are technically into a subview now, we'll have 129 | control of < and >. */ 130 | InputEvent ii = {.type = InputTypePress, .key = InputKeyDown}; 131 | ui_process_subview_updown(app,ii,2); 132 | } else if (input.key == InputKeyDown) { 133 | select_next_decoder(app); 134 | } else if (input.key == InputKeyUp) { 135 | select_prev_decoder(app); 136 | } 137 | } 138 | } 139 | 140 | /* Called after the user typed the new field value in the keyboard. 141 | * Let's save it and remove the keyboard view. */ 142 | static void text_input_done_callback(void* context) { 143 | ProtoViewApp *app = context; 144 | BuildViewPrivData *privdata = app->view_privdata; 145 | 146 | if (field_set_from_string(privdata->fieldset->fields[privdata->cur_field], 147 | privdata->user_value, strlen(privdata->user_value)) == false) 148 | { 149 | ui_show_alert(app, "Invalid value", 1500); 150 | } 151 | 152 | free(privdata->user_value); 153 | privdata->user_value = NULL; 154 | ui_dismiss_keyboard(app); 155 | } 156 | 157 | /* Handles the effects of < and > keys in field editing mode. 158 | * Instead of force the user to enter the text input mode, delete 159 | * the old value, enter the one, we allow to increment and 160 | * decrement the current field in a much simpler way. 161 | * 162 | * The current filed is changed by 'incr' amount. */ 163 | static bool increment_current_field(ProtoViewApp *app, int incr) { 164 | BuildViewPrivData *privdata = app->view_privdata; 165 | ProtoViewFieldSet *fs = privdata->fieldset; 166 | ProtoViewField *f = fs->fields[privdata->cur_field]; 167 | return field_incr_value(f,incr); 168 | } 169 | 170 | /* Handle input for fields editing mode. */ 171 | static void process_input_set_fields(ProtoViewApp *app, InputEvent input) { 172 | BuildViewPrivData *privdata = app->view_privdata; 173 | ProtoViewFieldSet *fs = privdata->fieldset; 174 | 175 | if (input.type == InputTypeShort && input.key == InputKeyOk) { 176 | /* Show the keyboard to let the user type the new 177 | * value. */ 178 | if (privdata->user_value == NULL) 179 | privdata->user_value = malloc(USER_VALUE_LEN); 180 | field_to_string(privdata->user_value, USER_VALUE_LEN, 181 | fs->fields[privdata->cur_field]); 182 | ui_show_keyboard(app, privdata->user_value, USER_VALUE_LEN, 183 | text_input_done_callback); 184 | } else if (input.type == InputTypeShort && input.key == InputKeyDown) { 185 | privdata->cur_field = (privdata->cur_field+1) % fs->numfields; 186 | } else if (input.type == InputTypeShort && input.key == InputKeyUp) { 187 | if (privdata->cur_field == 0) 188 | privdata->cur_field = fs->numfields-1; 189 | else 190 | privdata->cur_field--; 191 | } else if (input.type == InputTypeShort && input.key == InputKeyRight) { 192 | increment_current_field(app,1); 193 | } else if (input.type == InputTypeShort && input.key == InputKeyLeft) { 194 | increment_current_field(app,-1); 195 | } else if (input.type == InputTypeRepeat && input.key == InputKeyRight) { 196 | // The reason why we don't use a large increment directly 197 | // is that certain field types only support +1 -1 increments. 198 | int times = 10; 199 | while(times--) increment_current_field(app,1); 200 | } else if (input.type == InputTypeRepeat && input.key == InputKeyLeft) { 201 | int times = 10; 202 | while(times--) increment_current_field(app,-1); 203 | } else if (input.type == InputTypeLong && input.key == InputKeyOk) { 204 | // Build the message in a fresh raw buffer. 205 | if (privdata->decoder->build_message) { 206 | RawSamplesBuffer *rs = raw_samples_alloc(); 207 | privdata->decoder->build_message(rs,privdata->fieldset); 208 | app->signal_decoded = false; // So that the new signal will be 209 | // accepted as the current signal. 210 | scan_for_signal(app,rs,5); 211 | raw_samples_free(rs); 212 | ui_show_alert(app,"Done: press back key",3000); 213 | } 214 | } 215 | } 216 | 217 | /* Handle input for the build message view. */ 218 | void process_input_build_message(ProtoViewApp *app, InputEvent input) { 219 | BuildViewPrivData *privdata = app->view_privdata; 220 | if (privdata->decoder) 221 | process_input_set_fields(app,input); 222 | else 223 | process_input_select_decoder(app,input); 224 | } 225 | 226 | /* Enter view callback. */ 227 | void view_enter_build_message(ProtoViewApp *app) { 228 | BuildViewPrivData *privdata = app->view_privdata; 229 | 230 | // When we enter the view, the current decoder is just set to zero. 231 | // Seek the next valid if needed. 232 | if (Decoders[privdata->cur_decoder]->get_fields == NULL) { 233 | select_next_decoder(app); 234 | } 235 | 236 | // However if there is currently a decoded message, and the 237 | // decoder of such message supports message building, let's 238 | // select it. 239 | if (app->signal_decoded && 240 | app->msg_info->decoder->get_fields && 241 | app->msg_info->decoder->build_message) 242 | { 243 | while(Decoders[privdata->cur_decoder] != app->msg_info->decoder) 244 | select_next_decoder(app); 245 | } 246 | } 247 | 248 | /* Called on exit for cleanup. */ 249 | void view_exit_build_message(ProtoViewApp *app) { 250 | BuildViewPrivData *privdata = app->view_privdata; 251 | if (privdata->fieldset) fieldset_free(privdata->fieldset); 252 | if (privdata->user_value) free(privdata->user_value); 253 | } 254 | -------------------------------------------------------------------------------- /view_direct_sampling.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | #include "app.h" 5 | #include 6 | 7 | static void direct_sampling_timer_start(ProtoViewApp *app); 8 | static void direct_sampling_timer_stop(ProtoViewApp *app); 9 | 10 | #define CAPTURED_BITMAP_BITS (128*64) 11 | #define CAPTURED_BITMAP_BYTES (CAPTURED_BITMAP_BITS/8) 12 | #define DEFAULT_USEC_PER_PIXEL 50 13 | #define USEC_PER_PIXEL_SMALL_CHANGE 5 14 | #define USEC_PER_PIXEL_LARGE_CHANGE 25 15 | #define USEC_PER_PIXEL_MIN 5 16 | #define USEC_PER_PIXEL_MAX 300 17 | typedef struct { 18 | uint8_t *captured; // Bitmap with the last captured screen. 19 | uint32_t captured_idx; // Current index to write into the bitmap 20 | uint32_t usec_per_pixel; // Number of useconds a pixel should represent 21 | bool show_usage_info; 22 | } DirectSamplingViewPrivData; 23 | 24 | /* Read directly from the G0 CC1101 pin, and draw a black or white 25 | * dot depending on the level. */ 26 | void render_view_direct_sampling(Canvas *const canvas, ProtoViewApp *app) { 27 | DirectSamplingViewPrivData *privdata = app->view_privdata; 28 | 29 | if (!app->direct_sampling_enabled && privdata->show_usage_info) { 30 | canvas_set_font(canvas, FontSecondary); 31 | canvas_draw_str(canvas,2,9, "Direct sampling displays the"); 32 | canvas_draw_str(canvas,2,18,"the captured signal in real"); 33 | canvas_draw_str(canvas,2,27,"time, like in a CRT TV set."); 34 | canvas_draw_str(canvas,2,36,"Use UP/DOWN to change the"); 35 | canvas_draw_str(canvas,2,45,"resolution (usec/pixel)."); 36 | canvas_set_font(canvas, FontPrimary); 37 | canvas_draw_str(canvas,5,60,"To start/stop, press OK"); 38 | return; 39 | } 40 | privdata->show_usage_info = false; 41 | 42 | /* Draw on screen. */ 43 | int idx = 0; 44 | for (int y = 0; y < 64; y++) { 45 | for (int x = 0; x < 128; x++) { 46 | bool level = bitmap_get(privdata->captured, 47 | CAPTURED_BITMAP_BYTES,idx++); 48 | if (level) canvas_draw_dot(canvas,x,y); 49 | } 50 | } 51 | 52 | char buf[32]; 53 | snprintf(buf,sizeof(buf),"%lu usec/px", privdata->usec_per_pixel); 54 | canvas_set_font(canvas, FontSecondary); 55 | canvas_draw_str_with_border(canvas,1,60,buf,ColorWhite,ColorBlack); 56 | } 57 | 58 | /* Handle input */ 59 | void process_input_direct_sampling(ProtoViewApp *app, InputEvent input) { 60 | DirectSamplingViewPrivData *privdata = app->view_privdata; 61 | 62 | if (input.type == InputTypePress && input.key == InputKeyOk) { 63 | app->direct_sampling_enabled = !app->direct_sampling_enabled; 64 | } 65 | 66 | if ((input.key == InputKeyUp || input.key == InputKeyDown) && 67 | (input.type == InputTypePress || input.type == InputTypeRepeat)) 68 | { 69 | uint32_t change = input.type == InputTypePress ? 70 | USEC_PER_PIXEL_SMALL_CHANGE : 71 | USEC_PER_PIXEL_LARGE_CHANGE; 72 | if (input.key == InputKeyUp) change = -change; 73 | privdata->usec_per_pixel += change; 74 | if (privdata->usec_per_pixel < USEC_PER_PIXEL_MIN) 75 | privdata->usec_per_pixel = USEC_PER_PIXEL_MIN; 76 | else if (privdata->usec_per_pixel > USEC_PER_PIXEL_MAX) 77 | privdata->usec_per_pixel = USEC_PER_PIXEL_MAX; 78 | /* Update the timer frequency. */ 79 | direct_sampling_timer_stop(app); 80 | direct_sampling_timer_start(app); 81 | } 82 | } 83 | 84 | /* Enter view. Stop the subghz thread to prevent access as we read 85 | * the CC1101 data directly. */ 86 | void view_enter_direct_sampling(ProtoViewApp *app) { 87 | /* Set view defaults. */ 88 | DirectSamplingViewPrivData *privdata = app->view_privdata; 89 | privdata->usec_per_pixel = DEFAULT_USEC_PER_PIXEL; 90 | privdata->captured = malloc(CAPTURED_BITMAP_BYTES); 91 | privdata->show_usage_info = true; 92 | 93 | if (app->txrx->txrx_state == TxRxStateRx && 94 | !app->txrx->debug_timer_sampling) 95 | { 96 | furi_hal_subghz_stop_async_rx(); 97 | 98 | /* To read data asynchronously directly from the view, we need 99 | * to put the CC1101 back into reception mode (the previous call 100 | * to stop the async RX will put it into idle) and configure the 101 | * G0 pin for reading. */ 102 | furi_hal_subghz_rx(); 103 | furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, 104 | GpioSpeedLow); 105 | } else { 106 | raw_sampling_worker_stop(app); 107 | } 108 | 109 | // Start the timer to capture raw data 110 | direct_sampling_timer_start(app); 111 | } 112 | 113 | /* Exit view. Restore the subghz thread. */ 114 | void view_exit_direct_sampling(ProtoViewApp *app) { 115 | DirectSamplingViewPrivData *privdata = app->view_privdata; 116 | if (privdata->captured) free(privdata->captured); 117 | app->direct_sampling_enabled = false; 118 | 119 | direct_sampling_timer_stop(app); 120 | 121 | /* Restart normal data feeding. */ 122 | if (app->txrx->txrx_state == TxRxStateRx && 123 | !app->txrx->debug_timer_sampling) 124 | { 125 | furi_hal_subghz_start_async_rx(protoview_rx_callback, NULL); 126 | } else { 127 | raw_sampling_worker_start(app); 128 | } 129 | } 130 | 131 | /* =========================== Timer implementation ========================= */ 132 | 133 | static void ds_timer_isr(void *ctx) { 134 | ProtoViewApp *app = ctx; 135 | DirectSamplingViewPrivData *privdata = app->view_privdata; 136 | 137 | if (app->direct_sampling_enabled) { 138 | bool level = furi_hal_gpio_read(&gpio_cc1101_g0); 139 | bitmap_set(privdata->captured,CAPTURED_BITMAP_BYTES, 140 | privdata->captured_idx,level); 141 | privdata->captured_idx = (privdata->captured_idx+1) % 142 | CAPTURED_BITMAP_BITS; 143 | } 144 | LL_TIM_ClearFlag_UPDATE(TIM2); 145 | } 146 | 147 | static void direct_sampling_timer_start(ProtoViewApp *app) { 148 | DirectSamplingViewPrivData *privdata = app->view_privdata; 149 | 150 | LL_TIM_InitTypeDef tim_init = { 151 | .Prescaler = 63, /* CPU frequency is ~64Mhz. */ 152 | .CounterMode = LL_TIM_COUNTERMODE_UP, 153 | .Autoreload = privdata->usec_per_pixel 154 | }; 155 | 156 | LL_TIM_Init(TIM2, &tim_init); 157 | LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL); 158 | LL_TIM_DisableCounter(TIM2); 159 | LL_TIM_SetCounter(TIM2, 0); 160 | furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, ds_timer_isr, app); 161 | LL_TIM_EnableIT_UPDATE(TIM2); 162 | LL_TIM_EnableCounter(TIM2); 163 | } 164 | 165 | static void direct_sampling_timer_stop(ProtoViewApp *app) { 166 | UNUSED(app); 167 | FURI_CRITICAL_ENTER(); 168 | LL_TIM_DisableCounter(TIM2); 169 | LL_TIM_DisableIT_UPDATE(TIM2); 170 | furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, NULL, NULL); 171 | LL_TIM_DeInit(TIM2); 172 | FURI_CRITICAL_EXIT(); 173 | } 174 | -------------------------------------------------------------------------------- /view_info.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | #include "app.h" 5 | #include 6 | #include 7 | 8 | /* This view has subviews accessible navigating up/down. This 9 | * enumaration is used to track the currently active subview. */ 10 | enum { 11 | SubViewInfoMain, 12 | SubViewInfoSave, 13 | SubViewInfoLast, /* Just a sentinel. */ 14 | }; 15 | 16 | /* Our view private data. */ 17 | #define SAVE_FILENAME_LEN 64 18 | typedef struct { 19 | /* Our save view displays an oscilloscope-alike resampled signal, 20 | * so that the user can see what they are saving. With left/right 21 | * you can move to next rows. Here we store where we are. */ 22 | uint32_t signal_display_start_row; 23 | char *filename; 24 | uint8_t cur_info_page; // Info page to display. Useful when there are 25 | // too many fields populated by the decoder that 26 | // a single page is not enough. 27 | } InfoViewPrivData; 28 | 29 | /* Draw the text label and value of the specified info field at x,y. */ 30 | static void render_info_field(Canvas *const canvas, 31 | ProtoViewField *f, uint8_t x, uint8_t y) 32 | { 33 | char buf[64]; 34 | char strval[32]; 35 | 36 | field_to_string(strval,sizeof(strval),f); 37 | snprintf(buf,sizeof(buf),"%s: %s", f->name, strval); 38 | canvas_set_font(canvas, FontSecondary); 39 | canvas_draw_str(canvas, x, y, buf); 40 | } 41 | 42 | /* Render the view with the detected message information. */ 43 | #define INFO_LINES_PER_PAGE 5 44 | static void render_subview_main(Canvas *const canvas, ProtoViewApp *app) { 45 | InfoViewPrivData *privdata = app->view_privdata; 46 | uint8_t pages = (app->msg_info->fieldset->numfields 47 | +(INFO_LINES_PER_PAGE-1)) / INFO_LINES_PER_PAGE; 48 | privdata->cur_info_page %= pages; 49 | uint8_t current_page = privdata->cur_info_page; 50 | char buf[32]; 51 | 52 | /* Protocol name as title. */ 53 | canvas_set_font(canvas, FontPrimary); 54 | uint8_t y = 8, lineheight = 10; 55 | 56 | if (pages > 1) { 57 | snprintf(buf,sizeof(buf),"%s %u/%u", app->msg_info->decoder->name, 58 | current_page+1, pages); 59 | canvas_draw_str(canvas, 0, y, buf); 60 | } else { 61 | canvas_draw_str(canvas, 0, y, app->msg_info->decoder->name); 62 | } 63 | y += lineheight; 64 | 65 | /* Draw the info fields. */ 66 | uint8_t max_lines = INFO_LINES_PER_PAGE; 67 | uint32_t j = current_page*max_lines; 68 | while (j < app->msg_info->fieldset->numfields) { 69 | render_info_field(canvas,app->msg_info->fieldset->fields[j++],0,y); 70 | y += lineheight; 71 | if (--max_lines == 0) break; 72 | } 73 | 74 | /* Draw a vertical "save" label. Temporary solution, to switch to 75 | * something better ASAP. */ 76 | y = 37; 77 | lineheight = 7; 78 | canvas_draw_str(canvas, 119, y, "s"); y += lineheight; 79 | canvas_draw_str(canvas, 119, y, "a"); y += lineheight; 80 | canvas_draw_str(canvas, 119, y, "v"); y += lineheight; 81 | canvas_draw_str(canvas, 119, y, "e"); y += lineheight; 82 | } 83 | 84 | /* Render view with save option. */ 85 | static void render_subview_save(Canvas *const canvas, ProtoViewApp *app) { 86 | InfoViewPrivData *privdata = app->view_privdata; 87 | 88 | /* Display our signal in digital form: here we don't show the 89 | * signal with the exact timing of the received samples, but as it 90 | * is in its logic form, in exact multiples of the short pulse length. */ 91 | uint8_t rows = 6; 92 | uint8_t rowheight = 11; 93 | uint8_t bitwidth = 4; 94 | uint8_t bitheight = 5; 95 | uint32_t idx = privdata->signal_display_start_row * (128/4); 96 | bool prevbit = false; 97 | for (uint8_t y = bitheight+12; y <= rows*rowheight; y += rowheight) { 98 | for (uint8_t x = 0; x < 128; x += 4) { 99 | bool bit = bitmap_get(app->msg_info->bits, 100 | app->msg_info->bits_bytes,idx); 101 | uint8_t prevy = y + prevbit*(bitheight*-1) - 1; 102 | uint8_t thisy = y + bit*(bitheight*-1) - 1; 103 | canvas_draw_line(canvas,x,prevy,x,thisy); 104 | canvas_draw_line(canvas,x,thisy,x+bitwidth-1,thisy); 105 | prevbit = bit; 106 | if (idx >= app->msg_info->pulses_count) { 107 | canvas_set_color(canvas, ColorWhite); 108 | canvas_draw_dot(canvas, x+1,thisy); 109 | canvas_draw_dot(canvas, x+3,thisy); 110 | canvas_set_color(canvas, ColorBlack); 111 | } 112 | idx++; // Draw next bit 113 | } 114 | } 115 | 116 | canvas_set_font(canvas, FontSecondary); 117 | canvas_draw_str(canvas, 0, 6, "ok: send, long ok: save"); 118 | } 119 | 120 | /* Render the selected subview of this view. */ 121 | void render_view_info(Canvas *const canvas, ProtoViewApp *app) { 122 | if (app->signal_decoded == false) { 123 | canvas_set_font(canvas, FontSecondary); 124 | canvas_draw_str(canvas, 30,36,"No signal decoded"); 125 | return; 126 | } 127 | 128 | ui_show_available_subviews(canvas,app,SubViewInfoLast); 129 | switch(app->current_subview[app->current_view]) { 130 | case SubViewInfoMain: render_subview_main(canvas,app); break; 131 | case SubViewInfoSave: render_subview_save(canvas,app); break; 132 | } 133 | } 134 | 135 | /* The user typed the file name. Let's save it and remove the keyboard 136 | * view. */ 137 | static void text_input_done_callback(void* context) { 138 | ProtoViewApp *app = context; 139 | InfoViewPrivData *privdata = app->view_privdata; 140 | 141 | FuriString *save_path = furi_string_alloc_printf( 142 | "%s/%s.sub", EXT_PATH("subghz"), privdata->filename); 143 | save_signal(app, furi_string_get_cstr(save_path)); 144 | furi_string_free(save_path); 145 | 146 | free(privdata->filename); 147 | privdata->filename = NULL; // Don't free it again on view exit 148 | ui_dismiss_keyboard(app); 149 | ui_show_alert(app, "Signal saved", 1500); 150 | } 151 | 152 | /* Replace all the occurrences of character c1 with c2 in the specified 153 | * string. */ 154 | void str_replace(char *buf, char c1, char c2) { 155 | char *p = buf; 156 | while(*p) { 157 | if (*p == c1) *p = c2; 158 | p++; 159 | } 160 | } 161 | 162 | /* Set a random filename the user can edit. */ 163 | void set_signal_random_filename(ProtoViewApp *app, char *buf, size_t buflen) { 164 | char suffix[6]; 165 | set_random_name(suffix,sizeof(suffix)); 166 | snprintf(buf,buflen,"%.10s-%s-%d",app->msg_info->decoder->name,suffix,rand()%1000); 167 | str_replace(buf,' ','_'); 168 | str_replace(buf,'-','_'); 169 | str_replace(buf,'/','_'); 170 | } 171 | 172 | /* ========================== Signal transmission =========================== */ 173 | 174 | /* This is the context we pass to the data yield callback for 175 | * asynchronous tx. */ 176 | typedef enum { 177 | SendSignalSendStartGap, 178 | SendSignalSendBits, 179 | SendSignalSendEndGap, 180 | SendSignalEndTransmission 181 | } SendSignalState; 182 | 183 | #define PROTOVIEW_SENDSIGNAL_START_GAP 10000 /* microseconds. */ 184 | #define PROTOVIEW_SENDSIGNAL_END_GAP 10000 /* microseconds. */ 185 | 186 | typedef struct { 187 | SendSignalState state; // Current state. 188 | uint32_t curpos; // Current bit position of data to send. 189 | ProtoViewApp *app; // App reference. 190 | uint32_t start_gap_dur; // Gap to send at the start. 191 | uint32_t end_gap_dur; // Gap to send at the end. 192 | } SendSignalCtx; 193 | 194 | /* Setup the state context for the callback responsible to feed data 195 | * to the subghz async tx system. */ 196 | static void send_signal_init(SendSignalCtx *ss, ProtoViewApp *app) { 197 | ss->state = SendSignalSendStartGap; 198 | ss->curpos = 0; 199 | ss->app = app; 200 | ss->start_gap_dur = PROTOVIEW_SENDSIGNAL_START_GAP; 201 | ss->end_gap_dur = PROTOVIEW_SENDSIGNAL_END_GAP; 202 | } 203 | 204 | /* Send signal data feeder callback. When the asynchronous transmission is 205 | * active, this function is called to return new samples from the currently 206 | * decoded signal in app->msg_info. The subghz subsystem aspects this function, 207 | * that is the data feeder, to return LevelDuration types (that is a structure 208 | * with level, that is pulse or gap, and duration in microseconds). 209 | * 210 | * The position into the transmission is stored in the context 'ctx', that 211 | * references a SendSignalCtx structure. 212 | * 213 | * In the SendSignalCtx structure 'ss' we remember at which bit of the 214 | * message we are, in ss->curoff. We also send a start and end gap in order 215 | * to make sure the transmission is clear. 216 | */ 217 | LevelDuration radio_tx_feed_data(void *ctx) { 218 | SendSignalCtx *ss = ctx; 219 | 220 | /* Send start gap. */ 221 | if (ss->state == SendSignalSendStartGap) { 222 | ss->state = SendSignalSendBits; 223 | return level_duration_make(0,ss->start_gap_dur); 224 | } 225 | 226 | /* Send data. */ 227 | if (ss->state == SendSignalSendBits) { 228 | uint32_t dur = 0, j; 229 | uint32_t level = 0; 230 | 231 | /* Let's see how many consecutive bits we have with the same 232 | * level. */ 233 | for (j = 0; ss->curpos+j < ss->app->msg_info->pulses_count; j++) { 234 | uint32_t l = bitmap_get(ss->app->msg_info->bits, 235 | ss->app->msg_info->bits_bytes, 236 | ss->curpos+j); 237 | if (j == 0) { 238 | /* At the first bit of this sequence, we store the 239 | * level of the sequence. */ 240 | level = l; 241 | dur += ss->app->msg_info->short_pulse_dur; 242 | continue; 243 | } 244 | 245 | /* As long as the level is the same, we update the duration. 246 | * Otherwise stop the loop and return this sample. */ 247 | if (l != level) break; 248 | dur += ss->app->msg_info->short_pulse_dur; 249 | } 250 | ss->curpos += j; 251 | 252 | /* If this was the last set of bits, change the state to 253 | * send the final gap. */ 254 | if (ss->curpos >= ss->app->msg_info->pulses_count) 255 | ss->state = SendSignalSendEndGap; 256 | return level_duration_make(level, dur); 257 | } 258 | 259 | /* Send end gap. */ 260 | if (ss->state == SendSignalSendEndGap) { 261 | ss->state = SendSignalEndTransmission; 262 | return level_duration_make(0,ss->end_gap_dur); 263 | } 264 | 265 | /* End transmission. Here state is guaranteed 266 | * to be SendSignalEndTransmission */ 267 | return level_duration_reset(); 268 | } 269 | 270 | /* Vibrate and produce a click sound when a signal is sent. */ 271 | void notify_signal_sent(ProtoViewApp *app) { 272 | static const NotificationSequence sent_seq = { 273 | &message_blue_255, 274 | &message_vibro_on, 275 | &message_note_g1, 276 | &message_delay_10, 277 | &message_sound_off, 278 | &message_vibro_off, 279 | &message_blue_0, 280 | NULL 281 | }; 282 | notification_message(app->notification, &sent_seq); 283 | } 284 | 285 | /* Handle input for the info view. */ 286 | void process_input_info(ProtoViewApp *app, InputEvent input) { 287 | /* If we don't have a decoded signal, we don't allow to go up/down 288 | * in the subviews: they are only useful when a loaded signal. */ 289 | if (app->signal_decoded && 290 | ui_process_subview_updown(app,input,SubViewInfoLast)) return; 291 | 292 | InfoViewPrivData *privdata = app->view_privdata; 293 | int subview = ui_get_current_subview(app); 294 | 295 | /* Main subview. */ 296 | if (subview == SubViewInfoMain) { 297 | if (input.type == InputTypeLong && input.key == InputKeyOk) { 298 | /* Reset the current sample to capture the next. */ 299 | reset_current_signal(app); 300 | } else if (input.type == InputTypeShort && input.key == InputKeyOk) { 301 | /* Show next info page. */ 302 | privdata->cur_info_page++; 303 | } 304 | } else if (subview == SubViewInfoSave) { 305 | /* Save subview. */ 306 | if (input.type == InputTypePress && input.key == InputKeyRight) { 307 | privdata->signal_display_start_row++; 308 | } else if (input.type == InputTypePress && input.key == InputKeyLeft) { 309 | if (privdata->signal_display_start_row != 0) 310 | privdata->signal_display_start_row--; 311 | } else if (input.type == InputTypeLong && input.key == InputKeyOk) 312 | { 313 | // We have have the buffer already allocated, in case the 314 | // user aborted with BACK a previous saving. 315 | if (privdata->filename == NULL) 316 | privdata->filename = malloc(SAVE_FILENAME_LEN); 317 | set_signal_random_filename(app,privdata->filename,SAVE_FILENAME_LEN); 318 | ui_show_keyboard(app, privdata->filename, SAVE_FILENAME_LEN, 319 | text_input_done_callback); 320 | } else if (input.type == InputTypeShort && input.key == InputKeyOk) { 321 | SendSignalCtx send_state; 322 | send_signal_init(&send_state,app); 323 | radio_tx_signal(app,radio_tx_feed_data,&send_state); 324 | notify_signal_sent(app); 325 | } 326 | } 327 | } 328 | 329 | /* Called on view exit. */ 330 | void view_exit_info(ProtoViewApp *app) { 331 | InfoViewPrivData *privdata = app->view_privdata; 332 | // When the user aborts the keyboard input, we are left with the 333 | // filename buffer allocated. 334 | if (privdata->filename) free(privdata->filename); 335 | } 336 | -------------------------------------------------------------------------------- /view_raw_signal.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | #include "app.h" 5 | 6 | /* Render the received signal. 7 | * 8 | * The screen of the flipper is 128 x 64. Even using 4 pixels per line 9 | * (where low level signal is one pixel high, high level is 4 pixels 10 | * high) and 4 pixels of spacing between the different lines, we can 11 | * plot comfortably 8 lines. 12 | * 13 | * The 'idx' argument is the first sample to render in the circular 14 | * buffer. */ 15 | void render_signal(ProtoViewApp *app, Canvas *const canvas, RawSamplesBuffer *buf, uint32_t idx) { 16 | canvas_set_color(canvas, ColorBlack); 17 | 18 | int rows = 8; 19 | uint32_t time_per_pixel = app->us_scale; 20 | uint32_t start_idx = idx; 21 | bool level = 0; 22 | uint32_t dur = 0, sample_num = 0; 23 | for (int row = 0; row < rows ; row++) { 24 | for (int x = 0; x < 128; x++) { 25 | int y = 3 + row*8; 26 | if (dur < time_per_pixel/2) { 27 | /* Get more data. */ 28 | raw_samples_get(buf, idx++, &level, &dur); 29 | sample_num++; 30 | } 31 | 32 | canvas_draw_line(canvas, x,y,x,y-(level*3)); 33 | 34 | /* Write a small triangle under the last sample detected. */ 35 | if (app->signal_bestlen != 0 && 36 | sample_num+start_idx == app->signal_bestlen+1) 37 | { 38 | canvas_draw_dot(canvas,x,y+2); 39 | canvas_draw_dot(canvas,x-1,y+3); 40 | canvas_draw_dot(canvas,x,y+3); 41 | canvas_draw_dot(canvas,x+1,y+3); 42 | sample_num++; /* Make sure we don't mark the next, too. */ 43 | } 44 | 45 | /* Remove from the current level duration the time we 46 | * just plot. */ 47 | if (dur > time_per_pixel) 48 | dur -= time_per_pixel; 49 | else 50 | dur = 0; 51 | } 52 | } 53 | } 54 | 55 | /* Raw pulses rendering. This is our default view. */ 56 | void render_view_raw_pulses(Canvas *const canvas, ProtoViewApp *app) { 57 | /* Show signal. */ 58 | render_signal(app, canvas, DetectedSamples, app->signal_offset); 59 | 60 | /* Show signal information. */ 61 | char buf[64]; 62 | snprintf(buf,sizeof(buf),"%luus", 63 | (unsigned long)DetectedSamples->short_pulse_dur); 64 | canvas_set_font(canvas, FontSecondary); 65 | canvas_draw_str_with_border(canvas, 97, 63, buf, ColorWhite, ColorBlack); 66 | if (app->signal_decoded) { 67 | canvas_set_font(canvas, FontPrimary); 68 | canvas_draw_str_with_border(canvas, 1, 61, app->msg_info->decoder->name, ColorWhite, ColorBlack); 69 | } 70 | } 71 | 72 | /* Handle input for the raw pulses view. */ 73 | void process_input_raw_pulses(ProtoViewApp *app, InputEvent input) { 74 | if (input.type == InputTypeRepeat) { 75 | /* Handle panning of the signal window. Long pressing 76 | * right will show successive samples, long pressing left 77 | * previous samples. */ 78 | if (input.key == InputKeyRight) app->signal_offset++; 79 | else if (input.key == InputKeyLeft) app->signal_offset--; 80 | } else if (input.type == InputTypeLong) { 81 | if (input.key == InputKeyOk) { 82 | /* Reset the current sample to capture the next. */ 83 | reset_current_signal(app); 84 | } 85 | } else if (input.type == InputTypeShort) { 86 | if (input.key == InputKeyOk) { 87 | app->signal_offset = 0; 88 | adjust_raw_view_scale(app,DetectedSamples->short_pulse_dur); 89 | } else if (input.key == InputKeyDown) { 90 | /* Rescaling. The set becomes finer under 50us per pixel. */ 91 | uint32_t scale_step = app->us_scale >= 50 ? 50 : 10; 92 | if (app->us_scale < 500) app->us_scale += scale_step; 93 | } else if (input.key == InputKeyUp) { 94 | uint32_t scale_step = app->us_scale > 50 ? 50 : 10; 95 | if (app->us_scale > 10) app->us_scale -= scale_step; 96 | } 97 | } 98 | } 99 | 100 | /* Adjust raw view scale depending on short pulse duration. */ 101 | void adjust_raw_view_scale(ProtoViewApp *app, uint32_t short_pulse_dur) { 102 | if (short_pulse_dur == 0) 103 | app->us_scale = PROTOVIEW_RAW_VIEW_DEFAULT_SCALE; 104 | else if (short_pulse_dur < 75) 105 | app->us_scale = 10; 106 | else if (short_pulse_dur < 145) 107 | app->us_scale = 30; 108 | else if (short_pulse_dur < 400) 109 | app->us_scale = 100; 110 | else if (short_pulse_dur < 1000) 111 | app->us_scale = 200; 112 | else 113 | app->us_scale = PROTOVIEW_RAW_VIEW_DEFAULT_SCALE; 114 | } 115 | -------------------------------------------------------------------------------- /view_settings.c: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved 2 | * See the LICENSE file for information about the license. */ 3 | 4 | #include "app.h" 5 | 6 | /* Renders a single view with frequency and modulation setting. However 7 | * this are logically two different views, and only one of the settings 8 | * will be highlighted. */ 9 | void render_view_settings(Canvas *const canvas, ProtoViewApp *app) { 10 | canvas_set_font(canvas, FontPrimary); 11 | if (app->current_view == ViewFrequencySettings) 12 | canvas_draw_str_with_border(canvas,1,10,"Frequency",ColorWhite,ColorBlack); 13 | else 14 | canvas_draw_str(canvas,1,10,"Frequency"); 15 | 16 | if (app->current_view == ViewModulationSettings) 17 | canvas_draw_str_with_border(canvas,70,10,"Modulation",ColorWhite,ColorBlack); 18 | else 19 | canvas_draw_str(canvas,70,10,"Modulation"); 20 | canvas_set_font(canvas, FontSecondary); 21 | canvas_draw_str(canvas,10,61,"Use up and down to modify"); 22 | 23 | if (app->txrx->debug_timer_sampling) 24 | canvas_draw_str(canvas,3,52,"(DEBUG timer sampling is ON)"); 25 | 26 | /* Show frequency. We can use big numbers font since it's just a number. */ 27 | if (app->current_view == ViewFrequencySettings) { 28 | char buf[16]; 29 | snprintf(buf,sizeof(buf),"%.2f",(double)app->frequency/1000000); 30 | canvas_set_font(canvas, FontBigNumbers); 31 | canvas_draw_str(canvas, 30, 40, buf); 32 | } else if (app->current_view == ViewModulationSettings) { 33 | int current = app->modulation; 34 | canvas_set_font(canvas, FontPrimary); 35 | canvas_draw_str(canvas, 33, 39, ProtoViewModulations[current].name); 36 | } 37 | } 38 | 39 | /* Handle input for the settings view. */ 40 | void process_input_settings(ProtoViewApp *app, InputEvent input) { 41 | if (input.type == InputTypeLong && input.key == InputKeyOk) { 42 | /* Long pressing to OK sets the default frequency and 43 | * modulation. */ 44 | app->frequency = subghz_setting_get_default_frequency(app->setting); 45 | app->modulation = 0; 46 | } else if (0 && input.type == InputTypeLong && input.key == InputKeyDown) { 47 | /* Long pressing to down switches between normal and debug 48 | * timer sampling mode. NOTE: this feature is disabled for users, 49 | * only useful for devs (if useful at all). */ 50 | 51 | /* We have to stop the previous sampling system. */ 52 | radio_rx_end(app); 53 | 54 | /* Then switch mode and start the new one. */ 55 | app->txrx->debug_timer_sampling = !app->txrx->debug_timer_sampling; 56 | radio_begin(app); 57 | radio_rx(app); 58 | } else if (input.type == InputTypePress && 59 | (input.key != InputKeyDown || input.key != InputKeyUp)) 60 | { 61 | /* Handle up and down to change frequency or modulation. */ 62 | if (app->current_view == ViewFrequencySettings) { 63 | size_t curidx = 0, i; 64 | size_t count = subghz_setting_get_frequency_count(app->setting); 65 | 66 | /* Scan the list of frequencies to check for the index of the 67 | * currently set frequency. */ 68 | for(i = 0; i < count; i++) { 69 | uint32_t freq = subghz_setting_get_frequency(app->setting,i); 70 | if (freq == app->frequency) { 71 | curidx = i; 72 | break; 73 | } 74 | } 75 | if (i == count) return; /* Should never happen. */ 76 | 77 | if (input.key == InputKeyUp) { 78 | curidx = curidx == 0 ? count-1 : curidx-1; 79 | } else if (input.key == InputKeyDown) { 80 | curidx = (curidx+1) % count; 81 | } else { 82 | return; 83 | } 84 | app->frequency = subghz_setting_get_frequency(app->setting,curidx); 85 | } else if (app->current_view == ViewModulationSettings) { 86 | uint32_t count = 0; 87 | uint32_t modid = app->modulation; 88 | 89 | while(ProtoViewModulations[count].name != NULL) count++; 90 | if (input.key == InputKeyUp) { 91 | modid = modid == 0 ? count-1 : modid-1; 92 | } else if (input.key == InputKeyDown) { 93 | modid = (modid+1) % count; 94 | } else { 95 | return; 96 | } 97 | app->modulation = modid; 98 | } 99 | } else { 100 | return; 101 | } 102 | 103 | /* Apply changes when switching to other views. */ 104 | app->txrx->freq_mod_changed = true; 105 | } 106 | 107 | /* When the user switches to some other view, if they changed the parameters 108 | * we need to restart the radio with the right frequency and modulation. */ 109 | void view_exit_settings(ProtoViewApp *app) { 110 | if (app->txrx->freq_mod_changed) { 111 | FURI_LOG_E(TAG, "Setting view, setting frequency/modulation to %lu %s", app->frequency, ProtoViewModulations[app->modulation].name); 112 | radio_rx_end(app); 113 | radio_begin(app); 114 | radio_rx(app); 115 | app->txrx->freq_mod_changed = false; 116 | } 117 | } 118 | --------------------------------------------------------------------------------