Files
edgetx-gx12/radio/src/targets/simu/sdl_simu.cpp
T

860 lines
21 KiB
C++

/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#define SDL_MAIN_HANDLED
#include <SDL.h>
#include <SDL_keycode.h>
#include <imgui.h>
#include <imgui_impl_sdl2.h>
#include <imgui_impl_sdlrenderer2.h>
#include <algorithm>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <regex>
#include <string>
#include "hal/adc_driver.h"
#include "hal/rotary_encoder.h"
#include "hal/switch_driver.h"
#if !SDL_VERSION_ATLEAST(2,0,19)
#error This backend requires SDL 2.0.19+ because of SDL_RenderGeometryRaw() function
#endif
#if defined(__clang__)
#pragma clang diagnostic ignored "-Wunused-function"
#elif defined(__GNUC__)
#pragma GCC diagnostic ignored "-Wunused-function"
#endif
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_STATIC
#include "stb_image.h"
#if defined(STBI_NO_STDIO)
#error "STBI_NO_STDIO is defined"
#endif
#include "simu.h"
#include "widgets.h"
#include "knobs.h"
#include "display.h"
#include "simuaudio.h"
#include "simpgmspace.h"
#include "hal/key_driver.h"
#include "hal/switch_driver.h"
#include "switches.h"
#include "audio.h"
#include "debug.h"
#include "edgetx.h"
#include "arg_parser.h"
#define TIMER_INTERVAL 10 // 10ms
static SDL_Window* window;
static SDL_Renderer* renderer;
static SDL_Texture* screen_frame_buffer;
static GimbalState stick_left = {{0.5f, 0.5f}, false};
static GimbalState stick_right = {{0.5f, 0.5f}, false};
static const unsigned char _icon_png[] = {
#include "icon.lbm"
};
#if defined(ROTARY_ENCODER_NAVIGATION)
extern volatile rotenc_t rotencValue;
#endif
int pots[MAX_POTS] = {0};
static bool handleKeyEvents(SDL_Event& event)
{
if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) {
const auto& key_event = event.key;
bool key_handled = false;
uint8_t key = 0;
switch(key_event.keysym.sym) {
case SDLK_ESCAPE:
key = KEY_EXIT;
key_handled = true;
break;
case SDLK_RETURN:
key = KEY_ENTER;
key_handled = true;
break;
case SDLK_LEFT:
if (keysGetSupported() & (1 << KEY_LEFT)) {
key = KEY_LEFT;
key_handled = true;
}
break;
case SDLK_RIGHT:
if (keysGetSupported() & (1 << KEY_RIGHT)) {
key = KEY_RIGHT;
key_handled = true;
}
break;
case SDLK_UP:
#if defined(ROTARY_ENCODER_NAVIGATION)
if (event.type == SDL_KEYDOWN) {
rotencValue -= ROTARY_ENCODER_GRANULARITY;
}
#else
if (keysGetSupported() & (1 << KEY_UP)) {
key = KEY_UP;
key_handled = true;
}
#endif
break;
case SDLK_DOWN:
#if defined(ROTARY_ENCODER_NAVIGATION)
if (event.type == SDL_KEYDOWN) {
rotencValue += ROTARY_ENCODER_GRANULARITY;
}
#else
if (keysGetSupported() & (1 << KEY_DOWN)) {
key = KEY_DOWN;
key_handled = true;
}
#endif
break;
case SDLK_PLUS:
if (keysGetSupported() & (1 << KEY_PLUS)) {
key = KEY_PLUS;
key_handled = true;
}
break;
case SDLK_MINUS:
if (keysGetSupported() & (1 << KEY_MINUS)) {
key = KEY_MINUS;
key_handled = true;
}
break;
case SDLK_PAGEUP:
if (keysGetSupported() & (1 << KEY_PAGEUP)) {
key = KEY_PAGEUP;
key_handled = true;
}
break;
case SDLK_PAGEDOWN:
if (keysGetSupported() & (1 << KEY_PAGEDN)) {
key = KEY_PAGEDN;
key_handled = true;
}
break;
case SDLK_m:
if (keysGetSupported() & (1 << KEY_MENU)) {
key = KEY_MENU;
key_handled = true;
} else if (keysGetSupported() & (1 << KEY_MODEL)) {
key = KEY_MODEL;
key_handled = true;
}
break;
case SDLK_s:
if (keysGetSupported() & (1 << KEY_SYS)) {
key = KEY_SYS;
key_handled = true;
}
break;
case SDLK_t:
if (keysGetSupported() & (1 << KEY_TELE)) {
key = KEY_TELE;
key_handled = true;
}
break;
case SDLK_l:
ImGui::StyleColorsLight();
break;
case SDLK_d:
ImGui::StyleColorsDark();
break;
case SDLK_r:
if (event.type == SDL_KEYUP) {
simuStop();
simuStart();
}
break;
default:
key_handled = false;
break;
}
if (key_handled) {
simuSetKey(key, key_event.type == SDL_KEYDOWN ? true : false);
}
return key_handled;
}
return false;
}
static void redraw();
static bool handleEvents()
{
SDL_Event event;
while (SDL_PollEvent(&event)) {
if(handleKeyEvents(event))
continue;
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT)
return false;
if (event.type == SDL_WINDOWEVENT &&
event.window.event == SDL_WINDOWEVENT_CLOSE &&
event.window.windowID == SDL_GetWindowID(window))
return false;
}
redraw();
return true;
}
static SDL_Surface* LoadImage(const unsigned char* pixels, size_t len)
{
// Read data
int32_t w, h, bpp;
void* data = stbi_load_from_memory(pixels, len, &w, &h, &bpp, 0);
if (!data) return NULL;
Uint32 format = (bpp == 3) ? SDL_PIXELFORMAT_RGB24 : SDL_PIXELFORMAT_RGBA32;
SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormat(0, w, h, bpp * 8, format);
if (surface) {
SDL_LockSurface(surface);
memcpy(surface->pixels, data, w * h * bpp);
SDL_UnlockSurface(surface);
}
stbi_image_free(data);
return surface;
}
static SDL_Texture* LoadTexture(SDL_Renderer* renderer, const unsigned char* pixels, size_t len)
{
SDL_Surface* surface = LoadImage(pixels, len);
if (!surface) return NULL;
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_FreeSurface(surface);
return texture;
}
static float switches_width()
{
const int max_sw_line = MAX_SWITCHES / 2;
const int sw_line = std::min((int)switchGetMaxSwitches(), max_sw_line);
return sw_line * 18 + (sw_line - 1) * 4;
}
static void draw_switches()
{
const float spacing = 4;
const ImVec2 sw_size(18, 50);
ImGui::PushID("switches");
{
static int switches[MAX_SWITCHES] = {0};
ImGui::BeginGroup();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f);
ImGui::PushStyleVar(ImGuiStyleVar_GrabRounding, 4.0f);
int sw_idx = 0;
for (int i = 0; i < switchGetMaxSwitches(); i++) {
if (sw_idx) ImGui::SameLine();
if (!switchIsCustomSwitch(i)) {
if (++sw_idx >= MAX_SWITCHES / 2) sw_idx = 0;
if (!SWITCH_EXISTS(i)) {
switches[i] = 0;
ImGui::Dummy(sw_size);
} else {
ImGui::PushID(i);
ImGui::VSliderInt("##sw", sw_size,
&switches[i], IS_CONFIG_3POS(i) ? 2 : 1,
0, "", ImGuiSliderFlags_NoInput);
if (ImGui::IsItemActive() || ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", switchGetDefaultName(i));
}
ImGui::PopID();
}
if (IS_CONFIG_3POS(i)) {
simuSetSwitch(i, switches[i] == 0 ? -1 : switches[i] == 1 ? 0 : 1);
} else {
simuSetSwitch(i, switches[i] == 0 ? -1 : 1);
}
}
}
ImGui::PopStyleVar(3);
ImGui::EndGroup();
}
ImGui::PopID();
}
#if defined(FUNCTION_SWITCHES)
static ImVec4 rgb2rgba(uint32_t col)
{
float r = (float)((col >> 16) & 0xff) / 255.0f;
float g = (float)((col >> 8) & 0xff) / 255.0f;
float b = (float)((col >> 0) & 0xff) / 255.0f;
return {r, g, b, 1.0f};
}
static inline ImVec4 blend(const ImVec4& a, const ImVec4& b)
{
float w0 = a.w + b.w;
return ImVec4((a.x * a.w + b.x * b.w) / w0, (a.y * a.w + b.y * b.w) / w0,
(a.z * a.w + b.z * b.w) / w0, 1.0f);
}
static void push_custom_switch_styles(int index)
{
int cfsIdx = switchGetCustomSwitchIdx(index);
bool sw_on = fsLedState(cfsIdx);
bool rgb_set = switchIsCustomSwitch(index);
ImVec4 rgb = rgb2rgba(fsGetLedRGB(cfsIdx));
ImVec4 btn, border;
if (sw_on) {
btn = rgb_set ? rgb : ImGui::GetStyleColorVec4(ImGuiCol_SliderGrab);
border = ImGui::GetStyleColorVec4(ImGuiCol_SliderGrab);
} else {
btn = rgb_set ? rgb : ImGui::GetStyleColorVec4(ImGuiCol_FrameBg);
border = ImGui::GetStyleColorVec4(ImGuiCol_FrameBg);
}
// color intensity
float btn_int = (btn.x + btn.y + btn.z) / 3.0f;
float inv_int = 1.0f - btn_int;
ImVec4 mask(inv_int, inv_int, inv_int, 0.3f);
ImGui::PushStyleColor(ImGuiCol_Button, btn);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, blend(btn, mask));
ImGui::PushStyleColor(ImGuiCol_Border, border);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 3.0f);
}
static void pop_custom_switch_styles()
{
ImGui::PopStyleColor(3);
ImGui::PopStyleVar();
}
static void draw_custom_switches()
{
const float spacing = 4;
const ImVec2 sw_size(18, 18);
ImGui::PushID("custom switches");
{
ImGui::BeginGroup();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f);
ImGui::PushStyleVar(ImGuiStyleVar_GrabRounding, 4.0f);
for (int i = 0, n = 0; i < switchGetMaxSwitches(); i += 1) {
if (switchIsCustomSwitch(i)) {
int cfsIdx = switchGetCustomSwitchIdx(i);
if (n) ImGui::SameLine();
ImGui::PushID(i);
push_custom_switch_styles(i);
ImGui::Button("##csw", sw_size);
pop_custom_switch_styles();
bool active = ImGui::IsItemActive();
if (active || ImGui::IsItemHovered()) {
const char* name = switchGetDefaultName(i);
const char* on_off = fsLedState(cfsIdx) ? "on" : "off";
ImGui::SetTooltip("%s (%s)", name, on_off);
}
simuSetSwitch(i, active ? 1 : -1);
ImGui::PopID();
n += 1;
}
}
ImGui::PopStyleVar(3);
ImGui::EndGroup();
}
ImGui::PopID();
}
#endif
static float gimbals_width()
{
return 244.0f;
}
static void draw_gimbals()
{
stick_left.lock_y = (g_eeGeneral.stickMode == 1);
stick_right.lock_y = (g_eeGeneral.stickMode == 0);
GimbalPair("#gimbals", stick_left, stick_right);
}
static float pots_width()
{
const float spacing = 2;
float pot_width = ImGui::GetTextLineHeight() * 4.0f;
int pots = std::min((int)adcGetMaxInputs(ADC_INPUT_FLEX), 5);
return pots * pot_width + (pots - 1) * spacing +
2 * ImGui::GetStyle().CellPadding.x;
}
static void draw_pots()
{
const float spacing = 2;
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
ImGui::PushID("pots");
{
ImGui::BeginGroup();
int pot_idx = 0;
for (int i = 0; i < adcGetMaxInputs(ADC_INPUT_FLEX); i++) {
if (pot_idx > 0) ImGui::SameLine();
if (++pot_idx >= 5) pot_idx = 0;
if (!IS_POT_AVAILABLE(i)) {
pots[i] = 0;
auto sz = ImGui::GetTextLineHeight() * 4.0f;
ImGui::Dummy({sz, sz});
} else {
ImGui::PushID(i);
auto flags = ImGuiKnobFlags_NoTitle | ImGuiKnobFlags_ValueTooltip |
ImGuiKnobFlags_TitleTooltip;
auto label = adcGetInputLabel(ADC_INPUT_FLEX, i);
switch(getPotType(i)) {
case FLEX_POT:
case FLEX_POT_CENTER:
case FLEX_SLIDER:
ImGuiKnobs::KnobInt(label, &pots[i], -100, 100, 1, "%d",
ImGuiKnobVariant_Tick, 0, flags);
break;
case FLEX_MULTIPOS:
ImGuiKnobs::KnobInt(label, &pots[i], 0, 5, 0.2f, "%d",
ImGuiKnobVariant_Stepped, 0, flags, 6);
break;
}
ImGui::PopID();
}
}
ImGui::EndGroup();
}
ImGui::PopID();
ImGui::PopStyleVar();
}
void draw_controls()
{
ImGui::BeginTable("controls", 3, ImGuiTableFlags_SizingFixedFit);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, switches_width());
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, gimbals_width());
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, pots_width());
ImGui::TableNextColumn();
draw_switches();
#if defined(FUNCTION_SWITCHES)
draw_custom_switches();
#endif
ImGui::TableNextColumn();
draw_gimbals();
ImGui::TableNextColumn();
draw_pots();
ImGui::EndTable();
}
ImU32 get_bg_color()
{
if (LCD_DEPTH < 16) {
if (isBacklightEnabled()) {
return IM_COL32(47, 123, 227, 255);
} else {
return IM_COL32(200, 200, 200, 255);
}
} else {
return IM_COL32_BLACK_TRANS;
}
}
static ImVec2 calc_screen_size()
{
float width, height;
height = ImGui::GetContentRegionAvail().y;
width = height * float(LCD_W) / float(LCD_H);
if (width > ImGui::GetContentRegionAvail().x) {
width = ImGui::GetContentRegionAvail().x;
height = width * float(LCD_H) / float(LCD_W);
}
return {width, height};
}
static void draw_screen()
{
const ScreenDesc desc = {
.width = LCD_W,
.height = LCD_H,
.is_dot_matrix = LCD_DEPTH == 1 || LCD_DEPTH == 4,
};
auto size = calc_screen_size();
SimuScreen(desc, (ImTextureID)screen_frame_buffer, size, get_bg_color(),
(LCD_DEPTH == 16 && !isBacklightEnabled()) ?
IM_COL32(0,0,0,127) : IM_COL32_BLACK_TRANS);
#if defined(HARDWARE_TOUCH)
ScreenMouseEvent touch_event;
if (SimuScreenMouseEvent(desc, touch_event)) {
if (touch_event.type == ScreenMouseEventType::MouseDown) {
touchPanelDown(touch_event.pos_x, touch_event.pos_y);
} else {
touchPanelUp();
}
}
#endif
}
static int get_used_window_height()
{
return (int)ImGui::GetCursorScreenPos().y;
}
static int calc_min_width()
{
return switches_width() + gimbals_width() + pots_width() +
6 * ImGui::GetStyle().CellPadding.x;
}
static void redraw()
{
refreshDisplay(screen_frame_buffer);
// Start the Dear ImGui frame
ImGui_ImplSDLRenderer2_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
static ImGuiWindowFlags flags =
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoTitleBar;
// Use full work area (without menu-bars, task-bars etc.)
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
bool show_win = true;
if (ImGui::Begin("Main window", &show_win, flags)) {
draw_controls();
draw_screen();
}
ImGui::End();
// Rendering
ImGui::Render();
ImGuiIO& io = ImGui::GetIO();
SDL_RenderSetScale(renderer, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
auto bg_col = ImGui::GetColorU32(ImGuiCol_WindowBg);
SDL_SetRenderDrawColor(renderer,
(bg_col >> IM_COL32_R_SHIFT) & 0xFF,
(bg_col >> IM_COL32_G_SHIFT) & 0xFF,
(bg_col >> IM_COL32_B_SHIFT) & 0xFF,
(bg_col >> IM_COL32_A_SHIFT) & 0xFF);
SDL_RenderClear(renderer);
ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData(), renderer);
SDL_RenderPresent(renderer);
// Adjust window size
if (show_win) {
int min_w = calc_min_width();
int w, h;
SDL_GetWindowMinimumSize(window, &w, &h);
if (w < min_w || h < 400) {
SDL_Log("Window width: %d < %d", w, min_w);
SDL_SetWindowMinimumSize(window, min_w, 400);
}
}
}
int default_input_mode()
{
#if defined(DEFAULT_MODE)
return DEFAULT_MODE - 1;
#else
return 0;
#endif
}
int find_input_mode()
{
// TODO: add support for path from command line
std::ifstream file("./RADIO/radio.yml");
if (!file) return default_input_mode();
std::regex re("^stickMode:\\s*(\\d+)");
std::string line;
while (std::getline(file, line)) {
std::smatch matches;
if (std::regex_search(line, matches, re) && matches.size() > 1) {
return std::stoi(matches[1].str());
}
}
return default_input_mode();
}
int main(int argc, char* argv[])
{
auto progname = std::filesystem::path(argv[0]).filename();
ArgumentParser args(progname.string());
if (!args.parse(argc, argv)) {
return 1;
}
if (args.isHelpRequested()) {
args.printHelp();
return 0;
}
int window_height = 600;
if (args.hasHeight()) {
window_height = args.getHeight();
}
int window_width = 800;
if (args.hasWidth()) {
window_width = args.getWidth();
}
// Setup SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) != 0) {
printf("Error: %s\n", SDL_GetError());
return -1;
}
simuAudioInit();
// From 2.0.18: Enable native IME.
#ifdef SDL_HINT_IME_SHOW_UI
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
#endif
// Create window with SDL_Renderer graphics context
int window_flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
window = SDL_CreateWindow("EdgeTx Simu", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, window_width, window_height,
window_flags);
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (renderer == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_RENDER, "cannot create SDL_Renderer!");
return 0;
}
SDL_RendererInfo info;
SDL_GetRendererInfo(renderer, &info);
SDL_Log("Current SDL_Renderer: %s", info.name);
SDL_Surface* icon = LoadImage(_icon_png, sizeof(_icon_png));
if (window && icon) {
SDL_SetWindowIcon(window, icon);
}
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
// Setup Dear ImGui style
ImGui::StyleColorsLight();
// ImGui::StyleColorsDark();
// Setup Platform/Renderer backends
ImGui_ImplSDL2_InitForSDLRenderer(window, renderer);
ImGui_ImplSDLRenderer2_Init(renderer);
// Create textures for radio screen
screen_frame_buffer =
SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_STREAMING, LCD_W, LCD_H);
if (!screen_frame_buffer) {
SDL_Log("Could not create frame buffer textures");
return 0;
}
// SDL_SetTextureScaleMode(screen_frame_buffer, SDL_ScaleModeBest);
SDL_SetTextureBlendMode(screen_frame_buffer, SDL_BLENDMODE_BLEND);
// Init gimbal safe position
int input_mode = find_input_mode();
switch (input_mode) {
case 0:
case 2:
stick_right.pos.y = 1.0f;
break;
case 1:
case 3:
stick_left.pos.y = 1.0f;
break;
default:
SDL_Log("Invalid input mode %d", input_mode);
return 0;
}
// Init simulation
simuInit();
simuStart(true, args.getStoragePath().c_str(),
args.getSettingsPath().c_str());
// Main loop
SDL_SetEventFilter([](void*, SDL_Event* event){
if (event->type == SDL_WINDOWEVENT &&
(event->window.event == SDL_WINDOWEVENT_EXPOSED)) {
redraw();
return 0;
}
return 1;
}, NULL);
do {
Uint64 start_ts = SDL_GetPerformanceCounter();
if (!handleEvents()) break;
Uint64 end_ts = SDL_GetPerformanceCounter();
float elapsedMS =
(end_ts - start_ts) / (float)SDL_GetPerformanceFrequency() * 1000.0f;
// Cap to 60 FPS
SDL_Delay(std::max(0,(int32_t)floor(16.666f - elapsedMS)));
} while(true);
// App cleanup
simuStop();
// Cleanup
ImGui_ImplSDLRenderer2_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
SDL_DestroyTexture(screen_frame_buffer);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_FreeSurface(icon);
SDL_CloseAudio();
SDL_Quit();
return 0;
}
uint16_t simu_get_analog(uint8_t idx)
{
auto max_sticks = adcGetMaxInputs(ADC_INPUT_MAIN);
if (idx < max_sticks) {
switch(idx) {
case 0: return stick_left.pos.x * 4096;
case 1: return (1.0 - stick_left.pos.y) * 4096;
case 2: return (1.0 - stick_right.pos.y) * 4096;
case 3: return stick_right.pos.x * 4096;
}
}
idx -= max_sticks;
auto max_pots = adcGetMaxInputs(ADC_INPUT_FLEX);
if (idx < max_pots) {
switch(getPotType(idx)){
case FLEX_POT:
case FLEX_POT_CENTER:
case FLEX_SLIDER:
return uint16_t(((uint32_t(pots[idx]) + 100) * 4096) / 200);
case FLEX_MULTIPOS:
return (uint32_t(pots[idx]) * 4096) / 5;
}
}
// idx -= max_pots;
// auto max_axes = adcGetMaxInputs(ADC_INPUT_AXIS);
// if (idx < max_axes) return 0;
// probably RTC_BAT
return 0;
}