Merge commit '0b19fd29e665484223bcae5d53f339b832c4c9a1' into yeet-lfs

This commit is contained in:
Willy-JL
2024-08-12 18:05:58 +02:00
8 changed files with 221 additions and 6 deletions

View File

@@ -23,4 +23,4 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: "Check code formatting"
run: ./fbt lint lint_py
run: ./fbt lint_all

View File

@@ -11,7 +11,9 @@
- Furi: Re-enabled `FURI_TRACE` since LFS removal frees DFU, will get better crash messages with source code path (by @Willy-JL)
- OFW: RFID: Add GProxII support (by @BarTenderNZ)
- OFW: iButton: Support ID writing (by @Astrrra)
- OFW: FBT: Add `-Wundef` to compiler options (by @hedger)
- FBT:
- OFW: Add `-Wundef` to compiler options (by @hedger)
- OFW: Ensure that all images conform specification (by @skyhawkillusions & @hedger)
### Updated:
- Apps:
@@ -35,6 +37,7 @@
### Fixed:
- GUI: Fix Dark Mode after XOR canvas color, like in NFC dict attack (by @Willy-JL)
- OFW: NFC: Fix plantain balance string (by @Astrrra)
- OFW: Infrared: Fix cumulative error in infrared signals (by @gsurkov)
- OFW: JS: Ensure proper closure of variadic function in `mjs_array` (by @derskythe)
### Removed:

View File

@@ -322,7 +322,12 @@ firmware_env.Append(
"SConstruct",
"firmware.scons",
"fbt_options.py",
]
],
IMG_LINT_SOURCES=[
# Image assets
"applications",
"assets",
],
)
@@ -359,6 +364,39 @@ distenv.PhonyTarget(
PY_LINT_SOURCES=firmware_env["PY_LINT_SOURCES"],
)
# Image assets linting
distenv.PhonyTarget(
"lint_img",
[
[
"${PYTHON3}",
"${FBT_SCRIPT_DIR}/imglint.py",
"check",
"${IMG_LINT_SOURCES}",
"${ARGS}",
]
],
IMG_LINT_SOURCES=firmware_env["IMG_LINT_SOURCES"],
)
distenv.PhonyTarget(
"format_img",
[
[
"${PYTHON3}",
"${FBT_SCRIPT_DIR}/imglint.py",
"format",
"${IMG_LINT_SOURCES}",
"${ARGS}",
]
],
IMG_LINT_SOURCES=firmware_env["IMG_LINT_SOURCES"],
)
distenv.Alias("lint_all", ["lint", "lint_py", "lint_img"])
distenv.Alias("format_all", ["format", "format_py", "format_img"])
# Start Flipper CLI via PySerial's miniterm
distenv.PhonyTarget(
"cli",

View File

@@ -0,0 +1,8 @@
App(
appid="infrared_test",
name="Infrared Test",
apptype=FlipperAppType.DEBUG,
entry_point="infrared_test_app",
fap_category="Debug",
targets=["f7"],
)

View File

@@ -0,0 +1,61 @@
#include <furi.h>
#include <furi_hal_infrared.h>
#define TAG "InfraredTest"
#define CARRIER_FREQ_HZ (38000UL)
#define CARRIER_DUTY (0.33f)
#define BURST_DURATION_US (600UL)
#define BURST_COUNT (50UL)
typedef struct {
bool level;
uint32_t count;
} InfraredTestApp;
static FuriHalInfraredTxGetDataState
infrared_test_app_tx_data_callback(void* context, uint32_t* duration, bool* level) {
furi_assert(context);
furi_assert(duration);
furi_assert(level);
InfraredTestApp* app = context;
*duration = BURST_DURATION_US;
*level = app->level;
app->level = !app->level;
app->count += 1;
if(app->count < BURST_COUNT * 2) {
return FuriHalInfraredTxGetDataStateOk;
} else {
return FuriHalInfraredTxGetDataStateLastDone;
}
}
int32_t infrared_test_app(void* arg) {
UNUSED(arg);
InfraredTestApp app = {
.level = true,
};
FURI_LOG_I(TAG, "Starting test signal on PA7");
furi_hal_infrared_set_tx_output(FuriHalInfraredTxPinExtPA7);
furi_hal_infrared_async_tx_set_data_isr_callback(infrared_test_app_tx_data_callback, &app);
furi_hal_infrared_async_tx_start(CARRIER_FREQ_HZ, CARRIER_DUTY);
furi_hal_infrared_async_tx_wait_termination();
furi_hal_infrared_set_tx_output(FuriHalInfraredTxPinInternal);
FURI_LOG_I(TAG, "Test signal end");
FURI_LOG_I(
TAG,
"The measured signal should be %luus +-%.1fus",
(app.count - 1) * BURST_DURATION_US,
(double)1000000.0 / CARRIER_FREQ_HZ);
return 0;
}

View File

@@ -101,6 +101,8 @@ Currently `fbt` supports the default language server (`cpptools`) and `clangd`.
- `get_stlink` - output serial numbers for attached STLink probes. Used for specifying an adapter with `SWD_TRANSPORT_SERIAL=...`.
- `lint`, `format` - run `clang-format` on the C source code to check and reformat it according to the `.clang-format` specs. Supports `ARGS="..."` to pass extra arguments to clang-format.
- `lint_py`, `format_py` - run [black](https://black.readthedocs.io/en/stable/index.html) on the Python source code, build system files & application manifests. Supports `ARGS="..."` to pass extra arguments to black.
- `lint_img`, `format_img` - check the image assets for errors and format them. Enforces color depth and strips metadata.
- `lint_all`, `format_all` - run all linters and formatters.
- `firmware_pvs` - generate a PVS Studio report for the firmware. Requires PVS Studio to be available on your system's `PATH`.
- `doxygen` - generate Doxygen documentation for the firmware. `doxy` target also opens web browser to view the generated documentation.
- `cli` - start a Flipper CLI session over USB.

97
scripts/imglint.py Normal file
View File

@@ -0,0 +1,97 @@
import logging
import multiprocessing
import os
from pathlib import Path
from flipper.app import App
from PIL import Image, ImageOps
_logger = logging.getLogger(__name__)
def _check_image(image, do_fixup=False):
failed_checks = []
with Image.open(image) as img:
# check that is's pure 1-bit B&W
if img.mode != "1":
failed_checks.append(f"not 1-bit B&W, but {img.mode}")
if do_fixup:
img = img.convert("1")
# ...and does not have any metadata or ICC profile
if img.info:
failed_checks.append(f"has metadata")
if do_fixup:
img.info = {}
if do_fixup:
img.save(image)
_logger.info(f"Fixed image {image}")
if failed_checks:
_logger.warning(f"Image {image} issues: {'; '.join(failed_checks)}")
return len(failed_checks) == 0
class ImageLint(App):
ICONS_SUPPORTED_FORMATS = [".png"]
def init(self):
self.subparsers = self.parser.add_subparsers(help="sub-command help")
self.parser_check = self.subparsers.add_parser(
"check", help="Check image format and file names"
)
self.parser_check.add_argument("input", nargs="+")
self.parser_check.set_defaults(func=self.check)
self.parser_format = self.subparsers.add_parser(
"format", help="Format image and fix file names"
)
self.parser_format.add_argument(
"input",
nargs="+",
)
self.parser_format.set_defaults(func=self.format)
def _gather_images(self, folders):
images = []
for folder in folders:
for dirpath, _, filenames in os.walk(folder):
for filename in filenames:
if self.is_file_an_icon(filename):
images.append(os.path.join(dirpath, filename))
return images
def is_file_an_icon(self, filename):
extension = Path(filename).suffix.lower()
return extension in self.ICONS_SUPPORTED_FORMATS
def _process_images(self, images, do_fixup):
with multiprocessing.Pool() as pool:
image_checks = pool.starmap(
_check_image, [(image, do_fixup) for image in images]
)
return all(image_checks)
def check(self):
images = self._gather_images(self.args.input)
self.logger.info(f"Found {len(images)} images")
if not self._process_images(images, False):
self.logger.error("Some images are not in the correct format")
return 1
self.logger.info("All images are in the correct format")
return 0
def format(self):
images = self._gather_images(self.args.input)
self.logger.info(f"Found {len(images)} images")
if not self._process_images(images, True):
self.logger.warning("Applied fixes to some images")
else:
self.logger.info("All images were in the correct format")
return 0
if __name__ == "__main__":
ImageLint()()

View File

@@ -54,6 +54,7 @@ typedef struct {
typedef struct {
float cycle_duration;
float cycle_remainder;
FuriHalInfraredTxGetDataISRCallback data_callback;
FuriHalInfraredTxSignalSentISRCallback signal_sent_callback;
void* data_context;
@@ -512,7 +513,11 @@ static void furi_hal_infrared_tx_fill_buffer(uint8_t buf_num, uint8_t polarity_s
status = infrared_tim_tx.data_callback(infrared_tim_tx.data_context, &duration, &level);
uint32_t num_of_impulses = roundf(duration / infrared_tim_tx.cycle_duration);
const float num_of_impulses_f =
duration / infrared_tim_tx.cycle_duration + infrared_tim_tx.cycle_remainder;
const uint32_t num_of_impulses = roundf(num_of_impulses_f);
// Save the remainder (in carrier periods) for later use
infrared_tim_tx.cycle_remainder = num_of_impulses_f - num_of_impulses;
if(num_of_impulses == 0) {
if((*size == 0) && (status == FuriHalInfraredTxGetDataStateDone)) {
@@ -521,7 +526,7 @@ static void furi_hal_infrared_tx_fill_buffer(uint8_t buf_num, uint8_t polarity_s
*/
status = FuriHalInfraredTxGetDataStateOk;
}
} else if((num_of_impulses - 1) > 0xFFFF) {
} else if((num_of_impulses - 1) > UINT16_MAX) {
infrared_tim_tx.tx_timing_rest_duration = num_of_impulses - 1;
infrared_tim_tx.tx_timing_rest_status = status;
infrared_tim_tx.tx_timing_rest_level = level;
@@ -632,6 +637,7 @@ void furi_hal_infrared_async_tx_start(uint32_t freq, float duty_cycle) {
infrared_tim_tx.stop_semaphore = furi_semaphore_alloc(1, 0);
infrared_tim_tx.cycle_duration = 1000000.0 / freq;
infrared_tim_tx.tx_timing_rest_duration = 0;
infrared_tim_tx.cycle_remainder = 0;
furi_hal_infrared_tx_fill_buffer(0, INFRARED_POLARITY_SHIFT);
@@ -655,7 +661,7 @@ void furi_hal_infrared_async_tx_start(uint32_t freq, float duty_cycle) {
const GpioPin* tx_gpio = infrared_tx_pins[infrared_tx_output];
LL_GPIO_ResetOutputPin(tx_gpio->port, tx_gpio->pin); /* when disable it prevents false pulse */
furi_hal_gpio_init_ex(
tx_gpio, GpioModeAltFunctionPushPull, GpioPullUp, GpioSpeedHigh, GpioAltFn1TIM1);
tx_gpio, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedHigh, GpioAltFn1TIM1);
FURI_CRITICAL_ENTER();
LL_TIM_GenerateEvent_UPDATE(INFRARED_DMA_TIMER); /* TIMx_RCR -> Repetition counter */