Introduction️

Between the capabilities of the device Flipper Zero is the decoding of radio protocols below 1 GHz (Sub-GHz) frequencies. One of the available applications is Weather Station that decodes signals from temperature and humidity sensors. Due to the architecture of the application and the nature of the code project it is possible to add new protocols to the application.️

We will add the TWINS protocol based on ThermoPRO-TX4 and Polaroid based on inFactory-TH. We will also observe how to compile the application.️

Installation of uFBT and cloning of the repository️

First, we will install the tool uFBT️ (micro Flipper Build Tool), which allows us to compile applications individually.

$ python3 -m pip install --upgrade ufbt

We will clone the repository that contains the weather_station application, flipperzero-good-faps.

$ git clone https://github.com/flipperdevices/flipperzero-good-faps
$ cd flipperzero-good-faps

App modification

Accessing the application folder, weather_station, we find several folders. The files to be modified are located in the protocols folder.️

$ cd weather_station

For the TWINS protocol, we will create files twins.c and twins.h. For the Polaroid protocol, we will create files polaroid.c and polaroid.h. Finally, we will modify the file protocol_items.c and protocol_items.h to specify the new protocols. In the file protocol_items.c, we will add the protocols at the end of the array weather_station_protocol_registry_items in the following way:️

&ws_protocol_twins,
&ws_protocol_polaroid,

In the file protocol_items.h, we will specify the inclusion of the header files for the protocols in the following form:

#include "twins.h"
#include "polaroid.h"

Then is the content of the other files.️

twins.c

#include "twins.h"

#define TAG "WSProtocolTwins"

/*
 * Help (similar protocol to ThermoPRO-TX4)
 * https://github.com/merbanan/rtl_433/blob/master/src/devices/thermopro_tx2.c
 *
 * The sensor sends 36 bits 7 times, before the first packet there is a sync pulse.
 * The packets are ppm modulated (distance coding) with a pulse of ~500 us
 * followed by a short gap of ~2000 us for a 0 bit or a long ~4000 us gap for a
 * 1 bit, the sync gap is ~9000 us.
 * The data is grouped in 9 nibbles
 *     [type] [id0] [id1] [flags] [temp0] [temp1] [temp2] [humi0] [humi1]
 * - type: 4 bit fixed 1001 (9) or 0101 (5)
 * - id: 8 bit a random id that is generated when the sensor starts, could include battery status
 *   the same batteries often generate the same id
 * - flags(3): is 0 when the battery is low, otherwise 1 (ok)
 * - flags(2): is 1 when the sensor sends a reading when pressing the button on the sensor
 * - flags(1,0): the channel number that can be set by the sensor (1, 2, 3, X)
 * - temp: 12 bit signed scaled by 10
 * - humi: 8 bit always 11001100 (0xCC) if no humidity sensor is available
 * 
 */

#define TWINS_TYPE_1 0b1001
#define TWINS_TYPE_2 0b0101

static const SubGhzBlockConst ws_protocol_twins_const = {
    .te_short = 500,
    .te_long = 2000,
    .te_delta = 150,
    .min_count_bit_for_found = 36,
};

struct WSProtocolDecoderTwins {
    SubGhzProtocolDecoderBase base;

    SubGhzBlockDecoder decoder;
    WSBlockGeneric generic;
};

struct WSProtocolEncoderTwins {
    SubGhzProtocolEncoderBase base;

    SubGhzProtocolBlockEncoder encoder;
    WSBlockGeneric generic;
};

typedef enum {
    TwinsDecoderStepReset = 0,
    TwinsDecoderStepSaveDuration,
    TwinsDecoderStepCheckDuration,
} TwinsDecoderStep;

const SubGhzProtocolDecoder ws_protocol_twins_decoder = {
    .alloc = ws_protocol_decoder_twins_alloc,
    .free = ws_protocol_decoder_twins_free,

    .feed = ws_protocol_decoder_twins_feed,
    .reset = ws_protocol_decoder_twins_reset,

    .get_hash_data = ws_protocol_decoder_twins_get_hash_data,
    .serialize = ws_protocol_decoder_twins_serialize,
    .deserialize = ws_protocol_decoder_twins_deserialize,
    .get_string = ws_protocol_decoder_twins_get_string,
};

const SubGhzProtocolEncoder ws_protocol_twins_encoder = {
    .alloc = NULL,
    .free = NULL,

    .deserialize = NULL,
    .stop = NULL,
    .yield = NULL,
};

const SubGhzProtocol ws_protocol_twins = {
    .name = WS_PROTOCOL_TWINS_NAME,
    .type = SubGhzProtocolWeatherStation,
    .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_315 | SubGhzProtocolFlag_868 |
            SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable,

    .decoder = &ws_protocol_twins_decoder,
    .encoder = &ws_protocol_twins_encoder,
};

void* ws_protocol_decoder_twins_alloc(SubGhzEnvironment* environment) {
    UNUSED(environment);
    WSProtocolDecoderTwins* instance = malloc(sizeof(WSProtocolDecoderTwins));
    instance->base.protocol = &ws_protocol_twins;
    instance->generic.protocol_name = instance->base.protocol->name;
    return instance;
}

void ws_protocol_decoder_twins_free(void* context) {
    furi_assert(context);
    WSProtocolDecoderTwins* instance = context;
    free(instance);
}

void ws_protocol_decoder_twins_reset(void* context) {
    furi_assert(context);
    WSProtocolDecoderTwins* instance = context;
    instance->decoder.parser_step = TwinsDecoderStepReset;
}

static bool ws_protocol_twins_check(WSProtocolDecoderTwins* instance) {
    uint8_t type = instance->decoder.decode_data >> 32;

    if((type == TWINS_TYPE_1) || (type == TWINS_TYPE_2)) {
        return true;
    } else {
        return false;
    }
}

/**
 * Analysis of received data
 * @param instance Pointer to a WSBlockGeneric* instance
 */
static void ws_protocol_twins_remote_controller(WSBlockGeneric* instance) {
    instance->id = (instance->data >> 24) & 0xFF;
    instance->battery_low = !((instance->data >> 23) & 1);
    instance->btn = (instance->data >> 22) & 1;
    instance->channel = ((instance->data >> 20) & 0x03) + 1;

    if(!((instance->data >> 19) & 1)) {
        instance->temp = (float)((instance->data >> 8) & 0x07FF) / 10.0f;
    } else {
        instance->temp = (float)((~(instance->data >> 8) & 0x07FF) + 1) / -10.0f;
    }

    instance->humidity = (instance->data) & 0xFF;
}

void ws_protocol_decoder_twins_feed(void* context, bool level, uint32_t duration) {
    furi_assert(context);
    WSProtocolDecoderTwins* instance = context;

    switch(instance->decoder.parser_step) {
    case TwinsDecoderStepReset:
        if((!level) && (DURATION_DIFF(duration, ws_protocol_twins_const.te_short * 18) <
                        ws_protocol_twins_const.te_delta * 10)) {
            //Found sync
            instance->decoder.parser_step = TwinsDecoderStepSaveDuration;
            instance->decoder.decode_data = 0;
            instance->decoder.decode_count_bit = 0;
        }
        break;

    case TwinsDecoderStepSaveDuration:
        if(level) {
            instance->decoder.te_last = duration;
            instance->decoder.parser_step = TwinsDecoderStepCheckDuration;
        } else {
            instance->decoder.parser_step = TwinsDecoderStepReset;
        }
        break;

    case TwinsDecoderStepCheckDuration:
        if(!level) {
            if(DURATION_DIFF(duration, ws_protocol_twins_const.te_short * 18) <
               ws_protocol_twins_const.te_delta * 10) {
                //Found sync
                instance->decoder.parser_step = TwinsDecoderStepReset;
                if((instance->decoder.decode_count_bit ==
                    ws_protocol_twins_const.min_count_bit_for_found) &&
                   ws_protocol_twins_check(instance)) {
                    instance->generic.data = instance->decoder.decode_data;
                    instance->generic.data_count_bit = instance->decoder.decode_count_bit;
                    ws_protocol_twins_remote_controller(&instance->generic);
                    if(instance->base.callback)
                        instance->base.callback(&instance->base, instance->base.context);
                    instance->decoder.parser_step = TwinsDecoderStepCheckDuration;
                }
                instance->decoder.decode_data = 0;
                instance->decoder.decode_count_bit = 0;

                break;
            } else if(
                (DURATION_DIFF(instance->decoder.te_last, ws_protocol_twins_const.te_short) <
                 ws_protocol_twins_const.te_delta) &&
                (DURATION_DIFF(duration, ws_protocol_twins_const.te_long) <
                 ws_protocol_twins_const.te_delta * 2)) {
                subghz_protocol_blocks_add_bit(&instance->decoder, 0);
                instance->decoder.parser_step = TwinsDecoderStepSaveDuration;
            } else if(
                (DURATION_DIFF(instance->decoder.te_last, ws_protocol_twins_const.te_short) <
                 ws_protocol_twins_const.te_delta) &&
                (DURATION_DIFF(duration, ws_protocol_twins_const.te_long * 2) <
                 ws_protocol_twins_const.te_delta * 4)) {
                subghz_protocol_blocks_add_bit(&instance->decoder, 1);
                instance->decoder.parser_step = TwinsDecoderStepSaveDuration;
            } else {
                instance->decoder.parser_step = TwinsDecoderStepReset;
            }
        } else {
            instance->decoder.parser_step = TwinsDecoderStepReset;
        }
        break;
    }
}

uint8_t ws_protocol_decoder_twins_get_hash_data(void* context) {
    furi_assert(context);
    WSProtocolDecoderTwins* instance = context;
    return subghz_protocol_blocks_get_hash_data(
        &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1);
}

SubGhzProtocolStatus ws_protocol_decoder_twins_serialize(
    void* context,
    FlipperFormat* flipper_format,
    SubGhzRadioPreset* preset) {
    furi_assert(context);
    WSProtocolDecoderTwins* instance = context;
    return ws_block_generic_serialize(&instance->generic, flipper_format, preset);
}

SubGhzProtocolStatus
    ws_protocol_decoder_twins_deserialize(void* context, FlipperFormat* flipper_format) {
    furi_assert(context);
    WSProtocolDecoderTwins* instance = context;
    return ws_block_generic_deserialize_check_count_bit(
        &instance->generic, flipper_format, ws_protocol_twins_const.min_count_bit_for_found);
}

void ws_protocol_decoder_twins_get_string(void* context, FuriString* output) {
    furi_assert(context);
    WSProtocolDecoderTwins* instance = context;
    furi_string_printf(
        output,
        "%s %dbit\r\n"
        "Key:0x%lX%08lX\r\n"
        "Sn:0x%lX Ch:%d  Bat:%d\r\n"
        "Temp:%3.1f C Hum:%d%%",
        instance->generic.protocol_name,
        instance->generic.data_count_bit,
        (uint32_t)(instance->generic.data >> 31),
        (uint32_t)(instance->generic.data),
        instance->generic.id,
        instance->generic.channel,
        instance->generic.battery_low,
        (double)instance->generic.temp,
        instance->generic.humidity);
}

twins.h

#pragma once

#include <lib/subghz/protocols/base.h>

#include <lib/subghz/blocks/const.h>
#include <lib/subghz/blocks/decoder.h>
#include <lib/subghz/blocks/encoder.h>
#include "ws_generic.h"
#include <lib/subghz/blocks/math.h>

#define WS_PROTOCOL_TWINS_NAME "Twins"

typedef struct WSProtocolDecoderTwins WSProtocolDecoderTwins;
typedef struct WSProtocolEncoderTwins WSProtocolEncoderTwins;

extern const SubGhzProtocolDecoder ws_protocol_twins_decoder;
extern const SubGhzProtocolEncoder ws_protocol_twins_encoder;
extern const SubGhzProtocol ws_protocol_twins;

/**
 * Allocate WSProtocolDecoderTwins.
 * @param environment Pointer to a SubGhzEnvironment instance
 * @return WSProtocolDecoderTwins* pointer to a WSProtocolDecoderTwins instance
 */
void* ws_protocol_decoder_twins_alloc(SubGhzEnvironment* environment);

/**
 * Free WSProtocolDecoderTwins.
 * @param context Pointer to a WSProtocolDecoderTwins instance
 */
void ws_protocol_decoder_twins_free(void* context);

/**
 * Reset decoder WSProtocolDecoderTwins.
 * @param context Pointer to a WSProtocolDecoderTwins instance
 */
void ws_protocol_decoder_twins_reset(void* context);

/**
 * Parse a raw sequence of levels and durations received from the air.
 * @param context Pointer to a WSProtocolDecoderTwins instance
 * @param level Signal level true-high false-low
 * @param duration Duration of this level in, us
 */
void ws_protocol_decoder_twins_feed(void* context, bool level, uint32_t duration);

/**
 * Getting the hash sum of the last randomly received parcel.
 * @param context Pointer to a WSProtocolDecoderTwins instance
 * @return hash Hash sum
 */
uint8_t ws_protocol_decoder_twins_get_hash_data(void* context);

/**
 * Serialize data WSProtocolDecoderTwins.
 * @param context Pointer to a WSProtocolDecoderTwins instance
 * @param flipper_format Pointer to a FlipperFormat instance
 * @param preset The modulation on which the signal was received, SubGhzRadioPreset
 * @return status
 */
SubGhzProtocolStatus ws_protocol_decoder_twins_serialize(
    void* context,
    FlipperFormat* flipper_format,
    SubGhzRadioPreset* preset);

/**
 * Deserialize data WSProtocolDecoderTwins.
 * @param context Pointer to a WSProtocolDecoderTwins instance
 * @param flipper_format Pointer to a FlipperFormat instance
 * @return status
 */
SubGhzProtocolStatus
    ws_protocol_decoder_twins_deserialize(void* context, FlipperFormat* flipper_format);

/**
 * Getting a textual representation of the received data.
 * @param context Pointer to a WSProtocolDecoderTwins instance
 * @param output Resulting text
 */
void ws_protocol_decoder_twins_get_string(void* context, FuriString* output);

polaroid.c

#include "polaroid.h"

#define TAG "WSProtocolPolaroid"

/*
 * Help
 * https://github.com/merbanan/rtl_433/blob/master/src/devices/polaroid.c
 *
 * Analysis using Genuino (see http://gitlab.com/hp-uno, e.g. uno_log_433):
 * Observed On-Off-Key (OOK) data pattern:
 *     preamble            syncPrefix        data...(40 bit)                        syncPostfix
 *     HHLL HHLL HHLL HHLL HLLLLLLLLLLLLLLLL (HLLLL HLLLLLLLL HLLLL HLLLLLLLL ....) HLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
 * Breakdown:
 * - four preamble pairs '1'/'0' each with a length of ca. 1000us
 * - syncPre, syncPost, data0, data1 have a '1' start pulse of ca. 500us
 * - syncPre pulse before dataPtr has a '0' pulse length of ca. 8000us
 * - data0 (0-bits) have then a '0' pulse length of ca. 2000us
 * - data1 (1-bits) have then a '0' pulse length of ca. 4000us
 * - syncPost after dataPtr has a '0' pulse length of ca. 16000us
 * This analysis is the reason for the new r_device definitions below.
 * NB: pulse_slicer_ppm does not use .gap_limit if .tolerance is set.
 * 
 * Outdoor sensor, transmits temperature and humidity data
 * - inFactory NC-3982-913/NX-5817-902, Pearl (for FWS-686 station)
 * - nor-tec 73383 (weather station + sensor), Schou Company AS, Denmark
 * - DAY 73365 (weather station + sensor), Schou Company AS, Denmark
 * Known brand names: inFactory, nor-tec, GreenBlue, DAY. Manufacturer in China.
 * Transmissions includes an id. Every 60 seconds the sensor transmits 6 packets:
 *     0000 1111 | 0011 0000 | 0101 1100 | 1110 0111 | 0110 0001
 *     iiii iiii | cccc ub?? | tttt tttt | tttt hhhh | hhhh ??nn
 * - i: identification; changes on battery switch
 * - c: CRC-4; CCITT checksum, see below for computation specifics
 * - u: unknown; (sometimes set at power-on, but not always)
 * - b: battery low; flag to indicate low battery voltage
 * - h: Humidity; BCD-encoded, each nibble is one digit, 'A0' means 100%rH
 * - t: Temperature; in °F as binary number with one decimal place + 90 °F offset
 * - n: Channel; Channel number 1 - 3
 * 
 */

static const SubGhzBlockConst ws_protocol_polaroid_const = {
    .te_short = 500,
    .te_long = 2000,
    .te_delta = 150,
    .min_count_bit_for_found = 40,
};

struct WSProtocolDecoderPolaroid {
    SubGhzProtocolDecoderBase base;

    SubGhzBlockDecoder decoder;
    WSBlockGeneric generic;
};

struct WSProtocolEncoderPolaroid {
    SubGhzProtocolEncoderBase base;

    SubGhzProtocolBlockEncoder encoder;
    WSBlockGeneric generic;
};

typedef enum {
    PolaroidDecoderStepReset = 0,
    PolaroidDecoderStepCheckPreambule,
    PolaroidDecoderStepSaveDuration,
    PolaroidDecoderStepCheckDuration,
} PolaroidDecoderStep;

const SubGhzProtocolDecoder ws_protocol_polaroid_decoder = {
    .alloc = ws_protocol_decoder_polaroid_alloc,
    .free = ws_protocol_decoder_polaroid_free,

    .feed = ws_protocol_decoder_polaroid_feed,
    .reset = ws_protocol_decoder_polaroid_reset,

    .get_hash_data = ws_protocol_decoder_polaroid_get_hash_data,
    .serialize = ws_protocol_decoder_polaroid_serialize,
    .deserialize = ws_protocol_decoder_polaroid_deserialize,
    .get_string = ws_protocol_decoder_polaroid_get_string,
};

const SubGhzProtocolEncoder ws_protocol_polaroid_encoder = {
    .alloc = NULL,
    .free = NULL,

    .deserialize = NULL,
    .stop = NULL,
    .yield = NULL,
};

const SubGhzProtocol ws_protocol_polaroid = {
    .name = WS_PROTOCOL_POLAROID_NAME,
    .type = SubGhzProtocolWeatherStation,
    .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_315 | SubGhzProtocolFlag_868 |
            SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable,

    .decoder = &ws_protocol_polaroid_decoder,
    .encoder = &ws_protocol_polaroid_encoder,
};

void* ws_protocol_decoder_polaroid_alloc(SubGhzEnvironment* environment) {
    UNUSED(environment);
    WSProtocolDecoderPolaroid* instance = malloc(sizeof(WSProtocolDecoderPolaroid));
    instance->base.protocol = &ws_protocol_polaroid;
    instance->generic.protocol_name = instance->base.protocol->name;
    return instance;
}

void ws_protocol_decoder_polaroid_free(void* context) {
    furi_assert(context);
    WSProtocolDecoderPolaroid* instance = context;
    free(instance);
}

void ws_protocol_decoder_polaroid_reset(void* context) {
    furi_assert(context);
    WSProtocolDecoderPolaroid* instance = context;
    instance->decoder.parser_step = PolaroidDecoderStepReset;
}

static bool ws_protocol_polaroid_check_crc(WSProtocolDecoderPolaroid* instance) {
    uint8_t msg[] = {
        instance->decoder.decode_data >> 32,
        (((instance->decoder.decode_data >> 24) & 0x0F) | (instance->decoder.decode_data & 0x0F)
                                                              << 4),
        instance->decoder.decode_data >> 16,
        instance->decoder.decode_data >> 8,
        instance->decoder.decode_data};

    uint8_t crc =
        subghz_protocol_blocks_crc4(msg, 4, 0x13, 0); // Koopmann 0x9, CCITT-4; FP-4; ITU-T G.704
    crc ^= msg[4] >> 4; // last nibble is only XORed
    return (crc == ((instance->decoder.decode_data >> 28) & 0x0F));
}

/**
 * Analysis of received data
 * @param instance Pointer to a WSBlockGeneric* instance
 */
static void ws_protocol_polaroid_remote_controller(WSBlockGeneric* instance) {
    instance->id = instance->data >> 32;
    instance->battery_low = (instance->data >> 26) & 1;
    instance->btn = WS_NO_BTN;
    instance->temp =
        locale_fahrenheit_to_celsius(((float)((instance->data >> 12) & 0x0FFF) - 900.0f) / 10.0f);
    instance->humidity =
        (((instance->data >> 8) & 0x0F) * 10) + ((instance->data >> 4) & 0x0F); // BCD, 'A0'=100%rH
    instance->channel = instance->data & 0x03;
}

void ws_protocol_decoder_polaroid_feed(void* context, bool level, uint32_t duration) {
    furi_assert(context);
    WSProtocolDecoderPolaroid* instance = context;

    switch(instance->decoder.parser_step) {
    case PolaroidDecoderStepReset:
        if((level) && (DURATION_DIFF(duration, ws_protocol_polaroid_const.te_short) <
                       ws_protocol_polaroid_const.te_delta)) {
            instance->decoder.parser_step = PolaroidDecoderStepCheckPreambule;
            instance->decoder.te_last = duration;
        }
        break;

    case PolaroidDecoderStepCheckPreambule:
        if(level) {
            instance->decoder.te_last = duration;
        } else {
            if((DURATION_DIFF(instance->decoder.te_last, ws_protocol_polaroid_const.te_short) <
                ws_protocol_polaroid_const.te_delta) &&
               (DURATION_DIFF(duration, ws_protocol_polaroid_const.te_short * 16) <
                ws_protocol_polaroid_const.te_delta * 8)) {
                //Found syncPrefix
                instance->decoder.parser_step = PolaroidDecoderStepSaveDuration;
                instance->decoder.decode_data = 0;
                instance->decoder.decode_count_bit = 0;
            } else {
                instance->decoder.parser_step = PolaroidDecoderStepReset;
            }
        }
        break;

    case PolaroidDecoderStepSaveDuration:
        if(level) {
            instance->decoder.te_last = duration;
            instance->decoder.parser_step = PolaroidDecoderStepCheckDuration;
        } else {
            instance->decoder.parser_step = PolaroidDecoderStepReset;
        }
        break;

    case PolaroidDecoderStepCheckDuration:
        if(!level) {
            if(duration >= ((uint32_t)ws_protocol_polaroid_const.te_short * 30)) {
                //Found syncPostfix
                if((instance->decoder.decode_count_bit ==
                    ws_protocol_polaroid_const.min_count_bit_for_found) &&
                   ws_protocol_polaroid_check_crc(instance)) {
                    instance->generic.data = instance->decoder.decode_data;
                    instance->generic.data_count_bit = instance->decoder.decode_count_bit;
                    ws_protocol_polaroid_remote_controller(&instance->generic);
                    if(instance->base.callback)
                        instance->base.callback(&instance->base, instance->base.context);
                }
                instance->decoder.decode_data = 0;
                instance->decoder.decode_count_bit = 0;
                instance->decoder.parser_step = PolaroidDecoderStepReset;
                break;
            } else if(
                (DURATION_DIFF(instance->decoder.te_last, ws_protocol_polaroid_const.te_short) <
                 ws_protocol_polaroid_const.te_delta) &&
                (DURATION_DIFF(duration, ws_protocol_polaroid_const.te_long) <
                 ws_protocol_polaroid_const.te_delta * 2)) {
                subghz_protocol_blocks_add_bit(&instance->decoder, 0);
                instance->decoder.parser_step = PolaroidDecoderStepSaveDuration;
            } else if(
                (DURATION_DIFF(instance->decoder.te_last, ws_protocol_polaroid_const.te_short) <
                 ws_protocol_polaroid_const.te_delta) &&
                (DURATION_DIFF(duration, ws_protocol_polaroid_const.te_long * 2) <
                 ws_protocol_polaroid_const.te_delta * 4)) {
                subghz_protocol_blocks_add_bit(&instance->decoder, 1);
                instance->decoder.parser_step = PolaroidDecoderStepSaveDuration;
            } else {
                instance->decoder.parser_step = PolaroidDecoderStepReset;
            }
        } else {
            instance->decoder.parser_step = PolaroidDecoderStepReset;
        }
        break;
    }
}

uint8_t ws_protocol_decoder_polaroid_get_hash_data(void* context) {
    furi_assert(context);
    WSProtocolDecoderPolaroid* instance = context;
    return subghz_protocol_blocks_get_hash_data(
        &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1);
}

SubGhzProtocolStatus ws_protocol_decoder_polaroid_serialize(
    void* context,
    FlipperFormat* flipper_format,
    SubGhzRadioPreset* preset) {
    furi_assert(context);
    WSProtocolDecoderPolaroid* instance = context;
    return ws_block_generic_serialize(&instance->generic, flipper_format, preset);
}

SubGhzProtocolStatus
    ws_protocol_decoder_polaroid_deserialize(void* context, FlipperFormat* flipper_format) {
    furi_assert(context);
    WSProtocolDecoderPolaroid* instance = context;
    return ws_block_generic_deserialize_check_count_bit(
        &instance->generic, flipper_format, ws_protocol_polaroid_const.min_count_bit_for_found);
}

void ws_protocol_decoder_polaroid_get_string(void* context, FuriString* output) {
    furi_assert(context);
    WSProtocolDecoderPolaroid* instance = context;
    furi_string_printf(
        output,
        "%s %dbit\r\n"
        "Key:0x%lX%08lX\r\n"
        "Sn:0x%lX Ch:%d  Bat:%d\r\n"
        "Temp:%3.1f C Hum:%d%%",
        instance->generic.protocol_name,
        instance->generic.data_count_bit,
        (uint32_t)(instance->generic.data >> 32),
        (uint32_t)(instance->generic.data),
        instance->generic.id,
        instance->generic.channel,
        instance->generic.battery_low,
        (double)instance->generic.temp,
        instance->generic.humidity);
}

polaroid.h

#pragma once

#include <lib/subghz/protocols/base.h>

#include <lib/subghz/blocks/const.h>
#include <lib/subghz/blocks/decoder.h>
#include <lib/subghz/blocks/encoder.h>
#include "ws_generic.h"
#include <lib/subghz/blocks/math.h>

#define WS_PROTOCOL_POLAROID_NAME "Polaroid"

typedef struct WSProtocolDecoderPolaroid WSProtocolDecoderPolaroid;
typedef struct WSProtocolEncoderPolaroid WSProtocolEncoderPolaroid;

extern const SubGhzProtocolDecoder ws_protocol_polaroid_decoder;
extern const SubGhzProtocolEncoder ws_protocol_polaroid_encoder;
extern const SubGhzProtocol ws_protocol_polaroid;

/**
 * Allocate WSProtocolDecoderPolaroid.
 * @param environment Pointer to a SubGhzEnvironment instance
 * @return WSProtocolDecoderPolaroid* pointer to a WSProtocolDecoderPolaroid instance
 */
void* ws_protocol_decoder_polaroid_alloc(SubGhzEnvironment* environment);

/**
 * Free WSProtocolDecoderPolaroid.
 * @param context Pointer to a WSProtocolDecoderPolaroid instance
 */
void ws_protocol_decoder_polaroid_free(void* context);

/**
 * Reset decoder WSProtocolDecoderPolaroid.
 * @param context Pointer to a WSProtocolDecoderPolaroid instance
 */
void ws_protocol_decoder_polaroid_reset(void* context);

/**
 * Parse a raw sequence of levels and durations received from the air.
 * @param context Pointer to a WSProtocolDecoderPolaroid instance
 * @param level Signal level true-high false-low
 * @param duration Duration of this level in, us
 */
void ws_protocol_decoder_polaroid_feed(void* context, bool level, uint32_t duration);

/**
 * Getting the hash sum of the last randomly received parcel.
 * @param context Pointer to a WSProtocolDecoderPolaroid instance
 * @return hash Hash sum
 */
uint8_t ws_protocol_decoder_polaroid_get_hash_data(void* context);

/**
 * Serialize data WSProtocolDecoderPolaroid.
 * @param context Pointer to a WSProtocolDecoderPolaroid instance
 * @param flipper_format Pointer to a FlipperFormat instance
 * @param preset The modulation on which the signal was received, SubGhzRadioPreset
 * @return status
 */
SubGhzProtocolStatus ws_protocol_decoder_polaroid_serialize(
    void* context,
    FlipperFormat* flipper_format,
    SubGhzRadioPreset* preset);

/**
 * Deserialize data WSProtocolDecoderPolaroid.
 * @param context Pointer to a WSProtocolDecoderPolaroid instance
 * @param flipper_format Pointer to a FlipperFormat instance
 * @return status
 */
SubGhzProtocolStatus
    ws_protocol_decoder_polaroid_deserialize(void* context, FlipperFormat* flipper_format);

/**
 * Getting a textual representation of the received data.
 * @param context Pointer to a WSProtocolDecoderPolaroid instance
 * @param output Resulting text
 */
void ws_protocol_decoder_polaroid_get_string(void* context, FuriString* output);

Compilation️

Finally we will return to the main directory of the application, weather_station, and execute the command ufbt to compile the application.️

$ cd ..
$ ufbt

The resulting binary, weather_station.fap, will have been created in the dist directory. With the command ufbt flash, we can execute the application directly on the device connected via USB.️

Conclusion️

With these instructions we will be able to modify other applications in the future.️