feat(v16): add CI1302 voice control (VGR/VFL, motion control, menu navigation) (#7501)

Co-authored-by: Peter Feerick <5500713+pfeerick@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
helloradiosky
2026-09-20 11:48:49 +10:00
committed by GitHub
co-authored by Peter Feerick Claude Sonnet 5
parent f364c6ef63
commit 4aa9a258ee
26 changed files with 1625 additions and 13 deletions
+22 -1
View File
@@ -135,7 +135,8 @@ void BoardJson::afterLoadFixups(Board::Type board, InputsTable * inputs, Switche
}
// Flex switches are not listed in json file for these radios
int count = IS_RADIOMASTER_TX16S(board) || IS_RADIOMASTER_MT12(board) ? 2 : 0;
int count = IS_RADIOMASTER_TX16S(board) || IS_RADIOMASTER_MT12(board) ||
IS_HELLORADIOSKY_V16(board) ? 2 : 0;
for (int i = 1; i <= count; i++) {
QString tag = QString("FL%1").arg(i);
@@ -146,6 +147,26 @@ void BoardJson::afterLoadFixups(Board::Type board, InputsTable * inputs, Switche
switches->insert(switches->end(), defn);
}
}
// CI1302 voice control virtual switches are not listed in json file
// They must follow the flex switches above: firmware indexes them at
// switchGetMaxSwitches() + MAX_FLEX_SWITCHES
// VGR (voice gear) is a 2POS switch, VFL (voice flap) is 3POS -- see
// isTwoPosVoiceSwitch() in radio/src/drivers/CI1302.cpp
if (IS_HELLORADIOSKY_V16(board)) {
for (const auto &sw : {std::make_pair(QStringLiteral("VGR"), Board::SWITCH_2POS),
std::make_pair(QStringLiteral("VFL"), Board::SWITCH_3POS)}) {
const QString &tag = sw.first;
if (getSwitchIndex(switches, tag, Board::LVT_TAG) < 0) {
SwitchDefn defn;
defn.tag = tag.toStdString();
defn.name = defn.tag;
defn.type = sw.second;
defn.dflt = sw.second;
switches->insert(switches->end(), defn);
}
}
}
}
// called from Boards::getCapability if no capability match
@@ -180,7 +180,10 @@ RawSwitch YamlRawSwitchDecode(const std::string& sw_str)
val[val_len - 1] >= '0' && val[val_len - 1] <= '2') ||
(val_len >= 3 && val[0] == 'S' &&
val[1] >= 'A' && val[1] <= 'Z' &&
val[2] >= '0' && val[2] <= '2')) {
val[2] >= '0' && val[2] <= '2') ||
(val_len == 4 &&
(sw_str_tmp.substr(0, 3) == "VGR" || sw_str_tmp.substr(0, 3) == "VFL") &&
val[3] >= '0' && val[3] <= '2')) {
std::string tmp = sw_str_tmp.substr(0, val_len - 1);
+18 -5
View File
@@ -193,7 +193,7 @@ QString RawSource::toString(const ModelData * model, const GeneralSettings * con
};
static const QString special[] = {
"", tr("Batt"), tr("Time"), tr("GPS"), tr("Reserved1"), tr("Reserved2"), tr("Reserved3"), tr("Reserved4")
"", tr("Batt"), tr("Time"), tr("GPS"), tr("VGR"), tr("VFL"), tr("Reserved3"), tr("Reserved4")
};
static const QString rotary[] = { "", tr("REa"), tr("REb") };
@@ -355,8 +355,21 @@ bool RawSource::isAvailable(const ModelData * const model,
if (type == SOURCE_TYPE_SWITCH && abs(index) > b.getCapability(Board::Switches))
return false;
if (type == SOURCE_TYPE_SPECIAL && abs(index) >= SOURCE_TYPE_SPECIAL_FIRST_RESERVED)
return false;
if (type == SOURCE_TYPE_SPECIAL && abs(index) >= SOURCE_TYPE_SPECIAL_FIRST_RESERVED) {
// RESERVED1/2 are used as the CI1302 voice control VGR/VFL sources on this board
bool isVoiceSource = IS_HELLORADIOSKY_V16(board) &&
abs(index) <= SOURCE_TYPE_SPECIAL_RESERVED2;
if (!isVoiceSource)
return false;
// None on the Hardware Switches screen disables VGR/VFL as a source too
if (gs) {
QString tag = (abs(index) == SOURCE_TYPE_SPECIAL_RESERVED1) ? "VGR" : "VFL";
int swIdx = Boards::getSwitchIndex(tag, Board::LVT_TAG, board);
if (swIdx >= 0 && gs->switchConfig[swIdx].type == Board::SWITCH_NOT_AVAILABLE)
return false;
}
}
if (type == SOURCE_TYPE_TIMER && abs(index) > CPN_MAX_TIMERS)
return false;
@@ -475,8 +488,8 @@ tbl.insert(tbl.end(), {
{std::to_string(SOURCE_TYPE_SPECIAL_TX_BATT), "TX_VOLTAGE"},
{std::to_string(SOURCE_TYPE_SPECIAL_TX_TIME), "TX_TIME"},
{std::to_string(SOURCE_TYPE_SPECIAL_TX_GPS), "TX_GPS"},
{std::to_string(SOURCE_TYPE_SPECIAL_RESERVED1), "RESERVED1"},
{std::to_string(SOURCE_TYPE_SPECIAL_RESERVED2), "RESERVED2"},
{std::to_string(SOURCE_TYPE_SPECIAL_RESERVED1), "VGR"},
{std::to_string(SOURCE_TYPE_SPECIAL_RESERVED2), "VFL"},
{std::to_string(SOURCE_TYPE_SPECIAL_RESERVED3), "RESERVED3"},
{std::to_string(SOURCE_TYPE_SPECIAL_RESERVED4), "RESERVED4"},
});
@@ -0,0 +1,291 @@
/*
* 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.
*/
// CI1302 voice-control (VGR/VFL) source and switch coverage for the
// HelloRadioSky V16 board. Companion reimplements the firmware's YAML
// grammar independently, so these are the only guard against VGR/VFL
// silently decoding to NONE and getting lost on save.
#include "gtests.h"
#include <QByteArray>
#include "firmwares/eeprominterface.h"
#include "firmwares/edgetx/edgetxinterface.h"
#include "firmwares/edgetx/yaml_rawsource.h"
#include "firmwares/edgetx/yaml_rawswitch.h"
#include "firmwares/boardjson.h"
#include "firmwares/generalsettings.h"
#include "firmwares/mixdata.h"
namespace {
ModelData roundTrip(const ModelData& in, QByteArray& yamlOut)
{
writeModelToYaml(in, yamlOut);
ModelData out;
out.clear();
loadModelFromYaml(out, yamlOut);
return out;
}
class VoiceControlV16 : public ::testing::Test
{
protected:
void SetUp() override
{
Firmware::setCurrentVariant(Firmware::getFirmwareForFlavour("v16"));
ASSERT_NE(getCurrentFirmware(), nullptr)
<< "v16 (HelloRadioSky V16) firmware must be registered for this test";
ASSERT_TRUE(IS_HELLORADIOSKY_V16(getCurrentBoard()));
}
};
class VoiceControlOtherBoard : public ::testing::Test
{
protected:
void SetUp() override
{
Firmware::setCurrentVariant(Firmware::getFirmwareForFlavour("tx16s"));
ASSERT_NE(getCurrentFirmware(), nullptr)
<< "tx16s firmware must be registered for this test";
}
};
} // namespace
// VGR/VFL reuse the SOURCE_TYPE_SPECIAL RESERVED1/RESERVED2 slots; yaml tags
// must match the firmware's exactly ("VGR"/"VFL").
TEST_F(VoiceControlV16, MixSrcEncodeDecodeRoundTrips)
{
const RawSource vgr(SOURCE_TYPE_SPECIAL, SOURCE_TYPE_SPECIAL_RESERVED1);
const RawSource vfl(SOURCE_TYPE_SPECIAL, SOURCE_TYPE_SPECIAL_RESERVED2);
EXPECT_EQ(YamlRawSourceEncode(vgr), "VGR");
EXPECT_EQ(YamlRawSourceEncode(vfl), "VFL");
RawSource vgrBack = YamlRawSourceDecode("VGR");
EXPECT_EQ(vgrBack.type, SOURCE_TYPE_SPECIAL);
EXPECT_EQ(vgrBack.index, SOURCE_TYPE_SPECIAL_RESERVED1);
RawSource vflBack = YamlRawSourceDecode("VFL");
EXPECT_EQ(vflBack.type, SOURCE_TYPE_SPECIAL);
EXPECT_EQ(vflBack.index, SOURCE_TYPE_SPECIAL_RESERVED2);
}
// VGR/VFL are offered on the V16 board; RESERVED3/4 stay excluded.
TEST_F(VoiceControlV16, MixSrcAvailableOnlyForVoiceReservedSlots)
{
Board::Type board = getCurrentBoard();
const RawSource vgr(SOURCE_TYPE_SPECIAL, SOURCE_TYPE_SPECIAL_RESERVED1);
const RawSource vfl(SOURCE_TYPE_SPECIAL, SOURCE_TYPE_SPECIAL_RESERVED2);
const RawSource reserved3(SOURCE_TYPE_SPECIAL, SOURCE_TYPE_SPECIAL_RESERVED3);
const RawSource reserved4(SOURCE_TYPE_SPECIAL, SOURCE_TYPE_SPECIAL_RESERVED4);
EXPECT_TRUE(vgr.isAvailable(nullptr, nullptr, board));
EXPECT_TRUE(vfl.isAvailable(nullptr, nullptr, board));
EXPECT_FALSE(reserved3.isAvailable(nullptr, nullptr, board));
EXPECT_FALSE(reserved4.isAvailable(nullptr, nullptr, board));
}
// A mix already using VGR as its source must not keep tracking it once the
// user disables VGR on the Hardware Switches screen.
TEST_F(VoiceControlV16, MixSrcNotAvailableWhenSwitchDisabled)
{
Board::Type board = getCurrentBoard();
int vgrIdx = Boards::getSwitchYamlIndex(QStringLiteral("VGR"), BoardJson::YLT_REF, board);
ASSERT_GE(vgrIdx, 0);
const RawSource vgr(SOURCE_TYPE_SPECIAL, SOURCE_TYPE_SPECIAL_RESERVED1);
const RawSource vfl(SOURCE_TYPE_SPECIAL, SOURCE_TYPE_SPECIAL_RESERVED2);
GeneralSettings gs;
EXPECT_TRUE(vgr.isAvailable(nullptr, &gs, board));
gs.switchConfig[vgrIdx].type = Board::SWITCH_NOT_AVAILABLE;
EXPECT_FALSE(vgr.isAvailable(nullptr, &gs, board));
EXPECT_TRUE(vfl.isAvailable(nullptr, &gs, board));
}
// VGR/VFL must not leak into other radios' source pickers.
TEST_F(VoiceControlOtherBoard, MixSrcNotAvailableOnOtherBoards)
{
Board::Type board = getCurrentBoard();
ASSERT_FALSE(IS_HELLORADIOSKY_V16(board));
const RawSource vgr(SOURCE_TYPE_SPECIAL, SOURCE_TYPE_SPECIAL_RESERVED1);
const RawSource vfl(SOURCE_TYPE_SPECIAL, SOURCE_TYPE_SPECIAL_RESERVED2);
EXPECT_FALSE(vgr.isAvailable(nullptr, nullptr, board));
EXPECT_FALSE(vfl.isAvailable(nullptr, nullptr, board));
}
// Look the base switch index up rather than hardcoding it, since it depends
// on how many physical switches precede VGR/VFL in the table.
TEST_F(VoiceControlV16, SwitchEncodeDecodeRoundTrips)
{
Board::Type board = getCurrentBoard();
for (const QString& tag : {QStringLiteral("VGR"), QStringLiteral("VFL")}) {
int swIdx = Boards::getSwitchYamlIndex(tag, BoardJson::YLT_REF, board);
ASSERT_GE(swIdx, 0) << tag.toStdString() << " switch not registered for this board";
for (int pos = 0; pos < 3; pos++) {
RawSwitch rs(SWITCH_TYPE_SWITCH, swIdx * 3 + pos + 1);
std::string expected = QString(tag + QString::number(pos)).toStdString();
EXPECT_EQ(YamlRawSwitchEncode(rs), expected);
RawSwitch back = YamlRawSwitchDecode(expected);
EXPECT_TRUE(back == rs) << "round-trip mismatch for " << expected;
}
}
}
// A model using VGR as a mix source and VFL2 as its switch must survive a
// full YAML round-trip unchanged.
TEST_F(VoiceControlV16, ModelUsingVoiceSourceAndSwitchRoundTrips)
{
Board::Type board = getCurrentBoard();
int vflIdx = Boards::getSwitchYamlIndex(QStringLiteral("VFL"), BoardJson::YLT_REF, board);
ASSERT_GE(vflIdx, 0);
ModelData m;
m.clear();
m.used = true;
m.mixData[0].destCh = 1;
m.mixData[0].weight = 100;
m.mixData[0].srcRaw = RawSource(SOURCE_TYPE_SPECIAL, SOURCE_TYPE_SPECIAL_RESERVED1); // VGR
m.mixData[0].swtch = RawSwitch(SWITCH_TYPE_SWITCH, vflIdx * 3 + 2 + 1); // VFL2
QByteArray y1;
ModelData m2 = roundTrip(m, y1);
EXPECT_EQ(m2.mixData[0].destCh, m.mixData[0].destCh);
EXPECT_EQ(m2.mixData[0].srcRaw.type, m.mixData[0].srcRaw.type);
EXPECT_EQ(m2.mixData[0].srcRaw.index, m.mixData[0].srcRaw.index);
EXPECT_TRUE(m2.mixData[0].swtch == m.mixData[0].swtch)
<< "VFL2 switch was lost/changed on round-trip";
// Fixed point: re-serialising the reloaded model must not lose it either.
QByteArray y2;
ModelData m3 = roundTrip(m2, y2);
EXPECT_EQ(m3.mixData[0].srcRaw.index, m2.mixData[0].srcRaw.index);
EXPECT_TRUE(m3.mixData[0].swtch == m2.mixData[0].swtch);
}
// Simulator switch widgets address switches by index, so these must match
// firmware's initVoiceMappedSwitches() (radio/src/drivers/CI1302.cpp):
// switchGetMaxSwitches() + MAX_FLEX_SWITCHES, where V16 has 10 physical
// switches and 2 flex switches.
TEST_F(VoiceControlV16, VoiceSwitchIndexesFollowFlexSwitches)
{
Board::Type board = getCurrentBoard();
EXPECT_EQ(Boards::getCapability(board, Board::FlexSwitches), 2);
EXPECT_EQ(Boards::getSwitchYamlIndex(QStringLiteral("FL1"), BoardJson::YLT_REF, board), 10);
EXPECT_EQ(Boards::getSwitchYamlIndex(QStringLiteral("FL2"), BoardJson::YLT_REF, board), 11);
EXPECT_EQ(Boards::getSwitchYamlIndex(QStringLiteral("VGR"), BoardJson::YLT_REF, board), 12);
EXPECT_EQ(Boards::getSwitchYamlIndex(QStringLiteral("VFL"), BoardJson::YLT_REF, board), 13);
}
// VGR (voice gear) is a 2POS switch on real hardware -- see
// isTwoPosVoiceSwitch()/VSW_GEAR in radio/src/drivers/CI1302.cpp -- VFL
// (voice flap) is 3POS.
TEST_F(VoiceControlV16, VgrIsTwoPosVflIsThreePos)
{
Board::Type board = getCurrentBoard();
int vgrIdx = Boards::getSwitchYamlIndex(QStringLiteral("VGR"), BoardJson::YLT_REF, board);
int vflIdx = Boards::getSwitchYamlIndex(QStringLiteral("VFL"), BoardJson::YLT_REF, board);
ASSERT_GE(vgrIdx, 0);
ASSERT_GE(vflIdx, 0);
EXPECT_EQ(Boards::getSwitchInfo(vgrIdx, board).dflt, Board::SWITCH_2POS);
EXPECT_EQ(Boards::getSwitchInfo(vflIdx, board).dflt, Board::SWITCH_3POS);
}
// Firmware (radio/src/drivers/CI1302.cpp, CI1302_voiceSwitchSetDefaults())
// now default-initialises and genuinely persists VGR/VFL's type like any
// other switch, and its own settings.yml always includes an explicit entry
// for them (switchIsActive() already covers switchGetMaxAllSwitches()).
// A real device's file therefore always states VGR/VFL's type explicitly
// -- Companion must round-trip whatever value is there, including an
// explicit NONE if the user disabled one on the radio.
TEST_F(VoiceControlV16, GeneralSettingsRoundTripHonoursVoiceSwitchType)
{
Board::Type board = getCurrentBoard();
int vgrIdx = Boards::getSwitchYamlIndex(QStringLiteral("VGR"), BoardJson::YLT_REF, board);
int vflIdx = Boards::getSwitchYamlIndex(QStringLiteral("VFL"), BoardJson::YLT_REF, board);
ASSERT_GE(vgrIdx, 0);
ASSERT_GE(vflIdx, 0);
const QByteArray deviceYaml =
"board: v16\n"
"switchConfig:\n"
" SA:\n"
" type: 3POS\n"
" VGR:\n"
" type: NONE\n"
" VFL:\n"
" type: 3POS\n";
GeneralSettings gs;
ASSERT_TRUE(loadRadioSettingsFromYaml(gs, deviceYaml));
EXPECT_EQ(gs.switchConfig[vgrIdx].type, Board::SWITCH_NOT_AVAILABLE)
<< "the user disabled VGR on the radio; Companion must not resurrect it";
EXPECT_EQ(gs.switchConfig[vflIdx].type, Board::SWITCH_3POS);
RawSwitch vgrUp(SWITCH_TYPE_SWITCH, vgrIdx * 3 + 0 + 1);
RawSwitch vflUp(SWITCH_TYPE_SWITCH, vflIdx * 3 + 0 + 1);
RawSwitch vflMid(SWITCH_TYPE_SWITCH, vflIdx * 3 + 1 + 1);
RawSwitch vflDown(SWITCH_TYPE_SWITCH, vflIdx * 3 + 2 + 1);
EXPECT_FALSE(vgrUp.isAvailable(nullptr, &gs, board))
<< "VGR is configured NONE; it must not appear in the switches picker";
EXPECT_TRUE(vflUp.isAvailable(nullptr, &gs, board));
EXPECT_TRUE(vflMid.isAvailable(nullptr, &gs, board))
<< "VFL is a 3POS switch; all 3 positions must be available in the switches picker";
EXPECT_TRUE(vflDown.isAvailable(nullptr, &gs, board));
}
// A device's settings.yml with no switchConfig entry at all for VGR/VFL
// (e.g. hand-written, or predating the firmware default-init fix) falls
// back to None, same as it would for any other switch missing from the
// file -- there's no special-casing left for these two on the Companion side.
TEST_F(VoiceControlV16, GeneralSettingsRoundTripDefaultsMissingVoiceSwitchesToNone)
{
Board::Type board = getCurrentBoard();
int vgrIdx = Boards::getSwitchYamlIndex(QStringLiteral("VGR"), BoardJson::YLT_REF, board);
ASSERT_GE(vgrIdx, 0);
const QByteArray deviceYaml =
"board: v16\n"
"switchConfig:\n"
" SA:\n"
" type: 3POS\n";
GeneralSettings gs;
ASSERT_TRUE(loadRadioSettingsFromYaml(gs, deviceYaml));
EXPECT_EQ(gs.switchConfig[vgrIdx].type, Board::SWITCH_NOT_AVAILABLE);
}
+5 -1
View File
@@ -202,7 +202,11 @@ const char * const audioFilenames[] = {
"mixwarn3",
"timovr1",
"timovr2",
"timovr3"
"timovr3",
#if defined(VOICE_CONTROL_SENSOR)
"helloradio",
"okay",
#endif
};
constexpr unsigned int MAX_SWITCH_POSITIONS =
+5
View File
@@ -537,6 +537,11 @@ void audioTimerCountdown(uint8_t timer, int value);
#define AUDIO_TRAINER_LOST() audioEvent(AU_TRAINER_LOST)
#define AUDIO_TRAINER_BACK() audioEvent(AU_TRAINER_BACK)
#if defined(VOICE_CONTROL_SENSOR)
#define AUDIO_HELLORADIO() audioPlay(AU_HELLORADIO)
#define AUDIO_OKAY() audioPlay(AU_OKAY)
#endif
#else // AUDIO
#define AUDIO_TIMER_COUNTDOWN(idx, val)
+10
View File
@@ -527,6 +527,13 @@ enum MixSources {
MIXSRC_LIGHT,
#endif
// Must stay below MIXSRC_LAST or the mixer's source picker can't offer them
#if defined(VOICE_CONTROL_SENSOR)
MIXSRC_VGR,
MIXSRC_VFL,
MIXSRC_LAST_VOICE SKIP = MIXSRC_VFL,
#endif
MIXSRC_FIRST_HELI SKIP,
MIXSRC_LAST_HELI SKIP = MIXSRC_FIRST_HELI + 2,
@@ -598,6 +605,9 @@ enum SrcTypes {
SRC_TIMER = 1 << 17,
SRC_TELEM = 1 << 18,
SRC_LIGHT = 1 << 19,
#if defined(VOICE_CONTROL_SENSOR)
SRC_VOICE = 1 << 20,
#endif
SRC_NONE = 1 << 30,
SRC_INVERT = 1 << 31,
};
+890
View File
@@ -0,0 +1,890 @@
/*
* 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.
*/
#include "edgetx.h"
#include "CI1302.h"
#include "timers_driver.h"
#include "debug.h"
#include "audio.h"
#define VOICE_FRAME_DELAY_US 500
#define VOICE_START_BYTE 0x55
#define VOICE_STARTCN_BYTE 0xAA
#define VOICE_STARTEN_BYTE 0xAB
#define VOICE_FRAME_SIZE 6
#define VOICE_END_BYTE1 0xA5
#define VOICE_END_BYTE2 0x5A
#define VOICE_WKUP 0x01
#define VOICE_password 0x02
#define VOICE_Aileron_adjustment 0x03
#define VOICE_Pitch_adjustment 0x04
#define VOICE_Rotating_adjustment 0x05
#define VOICE_Up 0x0A
#define VOICE_Down 0x0B
#define VOICE_Left_one 0x14
#define VOICE_Right_one 0x15
#define VOICE_Confirm 0x1A
#define VOICE_Cancel 0x1B
#define VOICE_Model_Menu 0x21
#define VOICE_System_Menu 0x22
#define VOICE_Channel_Setup 0x23
#define VOICE_Channel_Monitor 0X32
#define VOICE_Internal_Module 0X33
#define VOICE_External_Module 0X34
#define VOICE_Aileron_Reverse 0x4A
#define VOICE_Elevator_Reverse 0x4B
#define VOICE_Rudder_Reverse 0x4C
#define VOICE_Motion_Control 0x55
#define VOICE_Telemetry 0x5B
#define VOICE_Open_Gear 0x7E
#define VOICE_Close_Gear 0x80
#define VOICE_Open_Flap 0x82
#define VOICE_Close_Flap 0x85
#define VOICE_POWEROFF 0x9E
#define VOICE_Start_recording 0x88
#define VOICE_End_recording 0x89
#define VOICE_Photo 0x8A
#define VOICE_Take_a_Photo 0x8B
#define VOICE_go_home 0x8C
#define VOICE_GPS_Return 0x91
#define VOICE_hi_take_off 0x9F
#define VOICE_hi_land 0xA1
#define VOICE_Sensorstatus 0xA2
struct voice_cmd_inputs_t {
uint8_t VOICESTARTBYTE1;
uint8_t VOICELanguage;
uint8_t VOICECMD;
uint8_t VOICECMDR;
uint8_t VOICEENDBYTE1;
uint8_t VOICEENDBYTE2;
};
bool VoiceRunStatus=true;
bool VoiceGearStatus=false;
uint8_t VoiceFlapStatus=false;
static uint32_t wkup_delay=0;
#define wkup_delayms 10000
#if defined(VOICE_CONTROL_SENSOR)
static void voiceMotionControlEnable();
static void voiceControlGuiHandleCmd(uint8_t voiceCmd);
bool CI1302_voiceSwitchBootGuardActive();
#endif
void processVoiceFrame(uint8_t * voicedata, uint32_t size)
{
voice_cmd_inputs_t *voiceframe=(voice_cmd_inputs_t*)voicedata;
if (size != VOICE_FRAME_SIZE || voiceframe->VOICESTARTBYTE1 != VOICE_START_BYTE ||
voiceframe->VOICEENDBYTE1 != VOICE_END_BYTE1||
voiceframe->VOICEENDBYTE2 != VOICE_END_BYTE2) {
return;
}
if((voiceframe->VOICECMD+voiceframe->VOICECMDR)!=0xff)return;
#if defined(VOICE_CONTROL_SENSOR)
// Motion control: no wake word required — call directly anytime.
if ((voiceframe->VOICELanguage == VOICE_STARTEN_BYTE ||
voiceframe->VOICELanguage == VOICE_STARTCN_BYTE) &&
voiceframe->VOICECMD == VOICE_Motion_Control) {
voiceMotionControlEnable();
AUDIO_OKAY();
return;
}
if (voiceframe->VOICELanguage == VOICE_STARTEN_BYTE || voiceframe->VOICELanguage == VOICE_STARTCN_BYTE) {
if(voiceframe->VOICECMD==VOICE_WKUP){
wkup_delay=timersGetMsTick();
AUDIO_HELLORADIO();
}
else if (voiceframe->VOICECMD == VOICE_Open_Gear) {
if (!CI1302_voiceSwitchBootGuardActive()) {
wkup_delay = timersGetMsTick();
VoiceGearStatus = true;
}
}
else if (voiceframe->VOICECMD == VOICE_Close_Gear) {
if (!CI1302_voiceSwitchBootGuardActive()) {
wkup_delay = timersGetMsTick();
VoiceGearStatus = false;
}
}
else if (voiceframe->VOICECMD == VOICE_Open_Flap) {
if (!CI1302_voiceSwitchBootGuardActive()) {
wkup_delay = timersGetMsTick();
if (VoiceFlapStatus < 2) {
VoiceFlapStatus++;
}
}
}
else if (voiceframe->VOICECMD == VOICE_Close_Flap) {
if (!CI1302_voiceSwitchBootGuardActive()) {
wkup_delay = timersGetMsTick();
if (VoiceFlapStatus > 0) {
VoiceFlapStatus--;
}
}
}
else if (voiceframe->VOICECMD == VOICE_Telemetry ||
voiceframe->VOICECMD == VOICE_Sensorstatus ||
voiceframe->VOICECMD == VOICE_Channel_Monitor ||
voiceframe->VOICECMD == VOICE_Internal_Module ||
voiceframe->VOICECMD == VOICE_External_Module ||
voiceframe->VOICECMD == VOICE_Model_Menu ||
voiceframe->VOICECMD == VOICE_System_Menu ||
voiceframe->VOICECMD == VOICE_Confirm ||
voiceframe->VOICECMD == VOICE_Cancel) {
wkup_delay = timersGetMsTick();
voiceControlGuiHandleCmd(voiceframe->VOICECMD);
}
}
#endif
}
// These call into the real UART driver (only compiled in for VOICE_CONTROL_USART
// && !defined(SIMU) below), so they're excluded from SIMU too.
#if !defined(SIMU)
extern "C" int GetVoiceInput(uint8_t *rxchar)
{
auto _getByte = voiceGetByte;
if (_getByte && (_getByte(rxchar) > 0)) {
return 0;
}
return -1;
}
void processUpdataInput(void)
{
uint8_t rxchar;
auto _getByte = dbgGetByte;
while (_getByte && (_getByte(&rxchar) > 0)) {
voiceSerialPutc(rxchar);
}
}
void processVoiceInput(void)
{
static uint8_t VoiceIndex = 0;
static uint32_t VoiceTimer;
static uint8_t VoiceFrame[VOICE_FRAME_SIZE];
uint32_t active = 0;
if(VoiceRunStatus==false)return;
uint8_t rxchar;
auto _getByte = voiceGetByte;
while (_getByte && (_getByte(&rxchar) > 0)) {
active = 1;
if (VoiceIndex > VOICE_FRAME_SIZE - 1) {
VoiceIndex = VOICE_FRAME_SIZE - 1;
}
VoiceFrame[VoiceIndex++] = rxchar;
TRACE("0x%x", rxchar);
}
if (wkup_delay)
{
if ((uint32_t)(timersGetMsTick() - wkup_delay) > wkup_delayms) {
wkup_delay = 0;
}
}
if (active) {
VoiceTimer = timersGetUsTick();
return;
}
if (VoiceIndex) {
if ((uint32_t)(timersGetUsTick() - VoiceTimer) > VOICE_FRAME_DELAY_US) {
processVoiceFrame(VoiceFrame, VoiceIndex);
VoiceIndex = 0;
}
}
}
#endif // !SIMU
#if defined(VOICE_CONTROL_SENSOR)
#include "drivers/CI1302_voice_integration.h"
#include "globals.h"
#include "gyro.h"
#include "input_mapping.h"
#include "keys.h"
#include "myeeprom.h"
#include "storage/yaml/yaml_bits.h"
#include <string.h>
#if defined(COLORLCD)
#include "module_setup.h"
#include "quick_menu.h"
#include "view_channels.h"
#include "view_main.h"
#include "LvglWrapper.h"
#endif
// --- motion control ---
bool MotionControlStatus = false;
static constexpr int16_t VOICE_MOTION_RELEASE_THRESHOLD = RESX / 4;
static constexpr uint8_t STICK_ROLE_ELE = 1;
static constexpr uint8_t STICK_ROLE_AIL = 3;
static int16_t gyroBaselineX = 0;
static int16_t gyroBaselineY = 0;
static void voiceMotionControlEnable()
{
#if defined(IMU)
gyroBaselineX = gyroScaledX();
gyroBaselineY = gyroScaledY();
#endif
MotionControlStatus = true;
}
void CI1302_voiceMotionControlApplyToInputs(uint8_t pots_offset)
{
#if defined(IMU)
if (!MotionControlStatus) {
return;
}
const int16_t deltaX = gyroScaledX() - gyroBaselineX;
const int16_t deltaY = gyroScaledY() - gyroBaselineY;
for (uint8_t phys = 0; phys < pots_offset; phys++) {
const uint8_t role = inputMappingConvertMode(phys);
const int16_t stick = calibratedAnalogs[phys];
if ((role == STICK_ROLE_AIL || role == STICK_ROLE_ELE) &&
abs(stick) > VOICE_MOTION_RELEASE_THRESHOLD) {
MotionControlStatus = false;
return;
}
}
for (uint8_t phys = 0; phys < pots_offset; phys++) {
const uint8_t role = inputMappingConvertMode(phys);
const int16_t stick = calibratedAnalogs[phys];
if (role == STICK_ROLE_AIL) {
calibratedAnalogs[phys] =
limit<int16_t>(-RESX, stick + deltaX, RESX);
} else if (role == STICK_ROLE_ELE) {
calibratedAnalogs[phys] =
limit<int16_t>(-RESX, stick + deltaY, RESX);
}
}
#else
(void)pots_offset;
MotionControlStatus = false;
#endif
}
// --- virtual voice switches (VGR / VFL) ---
enum VoiceSwitchId {
VSW_GEAR = 0,
VSW_FLAP,
};
static int8_t voiceSwitchIdx[VOICE_SWITCH_COUNT] = {
-2, -2,
};
static const char* const voiceSwitchNames[VOICE_SWITCH_COUNT] = {
"VGR", "VFL",
};
static constexpr uint32_t VOICE_BOOT_GUARD_MS = 5000;
static uint32_t VoiceBootGuardUntil = 0;
static void initVoiceMappedSwitches()
{
if (voiceSwitchIdx[VSW_GEAR] != -2) {
return;
}
int8_t base = (int8_t)(switchGetMaxSwitches() + MAX_FLEX_SWITCHES);
for (uint8_t i = 0; i < VOICE_SWITCH_COUNT; i++) {
voiceSwitchIdx[i] = base + i;
}
}
static int8_t voiceSwitchIndex(VoiceSwitchId id)
{
initVoiceMappedSwitches();
return voiceSwitchIdx[id];
}
uint8_t CI1302_voiceSwitchExtraCount()
{
return VOICE_SWITCH_COUNT;
}
bool CI1302_voiceSwitchIsIndex(uint8_t idx)
{
initVoiceMappedSwitches();
for (uint8_t i = 0; i < VOICE_SWITCH_COUNT; i++) {
if ((int8_t)idx == voiceSwitchIdx[i]) {
return true;
}
}
return false;
}
static VoiceSwitchId voiceSwitchIdFromIndex(uint8_t idx)
{
initVoiceMappedSwitches();
for (uint8_t i = 0; i < VOICE_SWITCH_COUNT; i++) {
if ((int8_t)idx == voiceSwitchIdx[i]) {
return (VoiceSwitchId)i;
}
}
return VSW_GEAR;
}
static bool isTwoPosVoiceSwitch(VoiceSwitchId id)
{
return id != VSW_FLAP;
}
// Whether the user has this voice switch configured as anything but None.
static bool voiceSwitchIsEnabled(VoiceSwitchId id)
{
int8_t idx = voiceSwitchIndex(id);
if (idx < 0 || idx >= MAX_SWITCHES) {
return false;
}
return g_eeGeneral.switchConfig[idx].type != SWITCH_NONE;
}
bool CI1302_voiceSwitchIsPositionAvailable(uint8_t idx, uint8_t position)
{
if (!CI1302_voiceSwitchIsIndex(idx)) {
return false;
}
if (isTwoPosVoiceSwitch(voiceSwitchIdFromIndex(idx)) && position == 1) {
return false;
}
return true;
}
#if defined(SIMU)
SwitchHwPos boardSwitchGetPosition(uint8_t idx);
#endif
static SwitchHwPos voiceSwitchPosition(uint8_t idx)
{
#if defined(SIMU)
// No CI1302 in the simulator: use the simulated switch widgets instead.
return boardSwitchGetPosition(idx);
#endif
switch (voiceSwitchIdFromIndex(idx)) {
case VSW_GEAR:
return VoiceGearStatus ? SWITCH_HW_DOWN : SWITCH_HW_UP;
case VSW_FLAP: {
uint8_t flapPos = (VoiceFlapStatus >= 2) ? 2 : VoiceFlapStatus;
return (SwitchHwPos)flapPos;
}
default:
return SWITCH_HW_UP;
}
}
bool CI1302_voiceSwitchTryGetPosition(uint8_t idx, SwitchHwPos* pos)
{
if (!CI1302_voiceSwitchIsIndex(idx)) {
return false;
}
*pos = voiceSwitchPosition(idx);
return true;
}
const char* CI1302_voiceSwitchTryGetName(uint8_t idx)
{
initVoiceMappedSwitches();
for (uint8_t i = 0; i < VOICE_SWITCH_COUNT; i++) {
if ((int8_t)idx == voiceSwitchIdx[i]) {
return voiceSwitchNames[i];
}
}
return nullptr;
}
int8_t CI1302_voiceSwitchTryGetIndexFromName(const char* name)
{
for (uint8_t i = 0; i < VOICE_SWITCH_COUNT; i++) {
if (strcmp(name, voiceSwitchNames[i]) == 0) {
return voiceSwitchIndex((VoiceSwitchId)i);
}
}
return -1;
}
bool CI1302_voiceSwitchTryGetHwType(uint8_t idx, SwitchHwType* type)
{
if (!CI1302_voiceSwitchIsIndex(idx)) {
return false;
}
// VGR is 2POS (bool), VFL is 3POS.
*type = isTwoPosVoiceSwitch(voiceSwitchIdFromIndex(idx)) ? SWITCH_HW_2POS
: SWITCH_HW_3POS;
return true;
}
// Nothing else default-inits switchConfig[] for these two indices.
void CI1302_voiceSwitchSetDefaults()
{
for (uint8_t i = 0; i < VOICE_SWITCH_COUNT; i++) {
int8_t idx = voiceSwitchIndex((VoiceSwitchId)i);
if (idx < 0 || idx >= MAX_SWITCHES) {
continue;
}
g_eeGeneral.switchConfig[idx].type =
isTwoPosVoiceSwitch((VoiceSwitchId)i) ? SWITCH_2POS : SWITCH_3POS;
g_eeGeneral.switchConfig[idx].name[0] = 0;
}
}
bool CI1302_voiceSwitchTryIsAvailable(int swtch, int context, bool* available)
{
int idx = swtch;
if (idx < 0) {
idx = -idx;
}
if (idx < SWSRC_FIRST_SWITCH || idx > SWSRC_LAST_SWITCH) {
return false;
}
idx -= SWSRC_FIRST_SWITCH;
div_t swinfo = div(idx, 3);
if (!CI1302_voiceSwitchIsIndex(swinfo.quot)) {
return false;
}
if (context == GeneralCustomFunctionsContext) {
*available = false;
return true;
}
if (!voiceSwitchIsEnabled(voiceSwitchIdFromIndex(swinfo.quot))) {
*available = false;
return true;
}
*available = CI1302_voiceSwitchIsPositionAvailable(swinfo.quot, swinfo.rem);
return true;
}
static bool voiceSwitchTryParseYamlTag(const char* val, uint8_t val_len,
const char* tag, VoiceSwitchId id,
int32_t* ival)
{
const size_t tagLen = strlen(tag);
if (val_len <= tagLen || strncmp(val, tag, tagLen) != 0) {
return false;
}
if (val[tagLen] < '0' || val[tagLen] > '2') {
return false;
}
int32_t sw = voiceSwitchIndex(id) * 3;
if (sw < 0) {
return false;
}
*ival = sw + yaml_str2int(val + tagLen, val_len - tagLen) + SWSRC_FIRST_SWITCH;
return true;
}
bool CI1302_voiceSwitchTryParseYamlSrc(const char* val, uint8_t val_len, int32_t* ival)
{
for (uint8_t i = 0; i < VOICE_SWITCH_COUNT; i++) {
if (voiceSwitchTryParseYamlTag(val, val_len, voiceSwitchNames[i],
(VoiceSwitchId)i, ival)) {
return true;
}
}
return false;
}
void CI1302_voiceSwitchInitBootGuard()
{
VoiceBootGuardUntil = timersGetMsTick() + VOICE_BOOT_GUARD_MS;
}
bool CI1302_voiceSwitchBootGuardActive()
{
return VoiceBootGuardUntil != 0 && timersGetMsTick() < VoiceBootGuardUntil;
}
bool CI1302_voiceSwitchSuppressMovedAudio(uint8_t idx, bool startup)
{
if (!CI1302_voiceSwitchIsIndex(idx)) {
return false;
}
return startup || CI1302_voiceSwitchBootGuardActive();
}
bool CI1302_voiceSwitchIsSwitchSource(swsrc_t swtch)
{
int idx = swtch;
if (idx < 0) {
idx = -idx;
}
if (idx < SWSRC_FIRST_SWITCH || idx > SWSRC_LAST_SWITCH) {
return false;
}
div_t swinfo = div(idx - SWSRC_FIRST_SWITCH, 3);
return CI1302_voiceSwitchIsIndex(swinfo.quot);
}
bool CI1302_voiceSwitchShouldSuppressFunctionAudio(swsrc_t swtch, bool risingEdge)
{
if (!risingEdge || !CI1302_voiceSwitchBootGuardActive()) {
return false;
}
return CI1302_voiceSwitchIsSwitchSource(swtch);
}
// --- voice menu navigation ---
#if defined(COLORLCD)
static bool voiceControlGuiOnMainScreen()
{
auto viewMain = ViewMain::instance();
if (!viewMain) {
return true;
}
Window* top = Window::topWindow();
if (!top) {
return true;
}
return top == viewMain || top == viewMain->getTopbar();
}
#endif
static void voiceControlGuiHandleCmd(uint8_t voiceCmd)
{
#if defined(COLORLCD)
switch (voiceCmd) {
case VOICE_Channel_Monitor:
AUDIO_OKAY();
new ChannelsViewMenu();
return;
case VOICE_Telemetry:
case VOICE_Sensorstatus:
AUDIO_OKAY();
QuickMenu::openPage(QM_MODEL_TELEMETRY);
return;
case VOICE_Internal_Module:
AUDIO_OKAY();
new ModulePage(INTERNAL_MODULE);
return;
case VOICE_External_Module:
AUDIO_OKAY();
new ModulePage(EXTERNAL_MODULE);
return;
case VOICE_Model_Menu:
AUDIO_OKAY();
QuickMenu::openPage(QM_MODEL_SETUP);
return;
case VOICE_System_Menu:
AUDIO_OKAY();
QuickMenu::openPage(QM_RADIO_SETUP);
return;
}
#endif
switch (voiceCmd) {
case VOICE_Confirm:
AUDIO_OKAY();
#if defined(COLORLCD)
if (voiceControlGuiOnMainScreen()) {
pushEvent(EVT_KEY_BREAK(KEY_ENTER));
} else {
lvglRequestKeyboardEnter();
}
#else
pushEvent(EVT_KEY_BREAK(KEY_ENTER));
#endif
break;
case VOICE_Cancel:
#if defined(COLORLCD)
if (voiceControlGuiOnMainScreen()) {
break;
}
#endif
AUDIO_OKAY();
pushEvent(EVT_KEY_BREAK(KEY_EXIT));
break;
}
}
// --- shared-code integration hooks ---
void CI1302_voiceIntegrationPer10ms()
{
#if !defined(SIMU)
processVoiceInput();
#endif
}
void CI1302_voiceIntegrationOnFlightReset()
{
CI1302_voiceSwitchInitBootGuard();
}
bool CI1302_voiceIntegrationMixSrcValue(mixsrc_t i, getvalue_t* val)
{
VoiceSwitchId id;
if (i == MIXSRC_VGR) {
id = VSW_GEAR;
} else if (i == MIXSRC_VFL) {
id = VSW_FLAP;
} else {
return false;
}
if (!voiceSwitchIsEnabled(id)) return false;
switch (voiceSwitchPosition(voiceSwitchIndex(id))) {
case SWITCH_HW_UP:
*val = -1024;
break;
case SWITCH_HW_MID:
*val = 0;
break;
default:
*val = 1024;
break;
}
return true;
}
bool CI1302_voiceIntegrationSourceAvailable(mixsrc_t i)
{
if (i == MIXSRC_VGR) return voiceSwitchIsEnabled(VSW_GEAR);
if (i == MIXSRC_VFL) return voiceSwitchIsEnabled(VSW_FLAP);
return false;
}
const char* CI1302_voiceIntegrationMixSrcName(mixsrc_t idx)
{
if (idx == MIXSRC_VGR) {
return "VGR";
}
if (idx == MIXSRC_VFL) {
return "VFL";
}
return nullptr;
}
bool CI1302_voiceIntegrationMixSrcParseYaml(const char* val, uint8_t val_len, uint32_t* out)
{
if (val_len == 3 && val[0] == 'V' && val[1] == 'G' && val[2] == 'R') {
*out = MIXSRC_VGR;
return true;
}
if (val_len == 3 && val[0] == 'V' && val[1] == 'F' && val[2] == 'L') {
*out = MIXSRC_VFL;
return true;
}
return false;
}
bool CI1302_voiceIntegrationMixSrcWriteYaml(uint32_t val, const char** str)
{
if (val == MIXSRC_VGR) {
*str = "VGR";
return true;
}
if (val == MIXSRC_VFL) {
*str = "VFL";
return true;
}
return false;
}
bool CI1302_voiceIntegrationSwitchSrcParseYaml(const char* val, uint8_t val_len, int32_t* ival)
{
return CI1302_voiceSwitchTryParseYamlSrc(val, val_len, ival);
}
bool CI1302_voiceIntegrationIsSwitchAvailable(int swtch, int context, bool* available)
{
return CI1302_voiceSwitchTryIsAvailable(swtch, context, available);
}
bool CI1302_voiceIntegrationIsSwitchSwitchAvailable(int swtch, bool* available)
{
return CI1302_voiceSwitchTryIsAvailable(swtch + SWSRC_FIRST_SWITCH, 0, available);
}
#endif // VOICE_CONTROL_SENSOR
#if defined(VOICE_CONTROL_USART) && !defined(SIMU)
#include "hal/gpio.h"
#include "hal/serial_driver.h"
#include "stm32_gpio.h"
#include "stm32_serial_driver.h"
#define VOICE_USART_IRQ_PRIORITY 6
#define VOICE_RX_BUFFER_SIZE 64
#define VOICE_TX_BUFFER_SIZE 1
static const stm32_usart_t _voiceUSART = {
.USARTx = VOICE_CONTROL_USART,
.txGPIO = VOICE_CONTROL_TX_GPIO,
.rxGPIO = VOICE_CONTROL_RX_GPIO,
.IRQn = VOICE_CONTROL_USART_IRQn,
.IRQ_Prio = VOICE_USART_IRQ_PRIORITY,
.txDMA = nullptr,
.txDMA_Stream = 0,
.txDMA_Channel = 0,
.rxDMA = nullptr,
.rxDMA_Stream = 0,
.rxDMA_Channel = 0,
};
DEFINE_STM32_SERIAL_PORT(VoiceControl, _voiceUSART, VOICE_RX_BUFFER_SIZE,
VOICE_TX_BUFFER_SIZE);
static void* _voice_usart_ctx = nullptr;
static bool _voice_power_gpio_inited = false;
extern "C" void voiceControlPowerOn(void)
{
if (!_voice_power_gpio_inited) {
gpio_init(VOICE_CONTROL_PWR_GPIO, GPIO_OUT, GPIO_PIN_SPEED_LOW);
_voice_power_gpio_inited = true;
}
gpio_set(VOICE_CONTROL_PWR_GPIO);
}
extern "C" void voiceControlPowerOff(void)
{
if (!_voice_power_gpio_inited) {
gpio_init(VOICE_CONTROL_PWR_GPIO, GPIO_OUT, GPIO_PIN_SPEED_LOW);
_voice_power_gpio_inited = true;
}
gpio_clear(VOICE_CONTROL_PWR_GPIO);
if (_voice_usart_ctx) {
STM32SerialDriver.deinit(_voice_usart_ctx);
_voice_usart_ctx = nullptr;
}
}
void voiceControlInit(void)
{
voiceControlPowerOn();
etx_serial_init cfg = {
.baudrate = VOICE_CTRL_BAUD,
.encoding = ETX_Encoding_8N1,
.direction = ETX_Dir_TX_RX,
.polarity = ETX_Pol_Normal,
};
if (!_voice_usart_ctx) {
_voice_usart_ctx =
STM32SerialDriver.init(REF_STM32_SERIAL_PORT(VoiceControl), &cfg);
} else {
STM32SerialDriver.setBaudrate(_voice_usart_ctx, VOICE_CTRL_BAUD);
}
#if defined(VOICE_CONTROL_SENSOR)
CI1302_voiceSwitchInitBootGuard();
#endif
}
extern "C" int voiceGetByte(uint8_t* byte)
{
if (!_voice_usart_ctx) {
return 0;
}
return STM32SerialDriver.getByte(_voice_usart_ctx, byte);
}
extern "C" void voiceSerialPutc(char c)
{
if (!_voice_usart_ctx) {
return;
}
STM32SerialDriver.sendByte(_voice_usart_ctx, (uint8_t)c);
}
extern "C" void voiceSerialPutstr(uint8_t* data, uint32_t len)
{
if (!_voice_usart_ctx || !data || !len) {
return;
}
STM32SerialDriver.sendBuffer(_voice_usart_ctx, data, len);
}
extern "C" int dbgGetByte(uint8_t* byte)
{
(void)byte;
return 0;
}
#endif // VOICE_CONTROL_USART
+57
View File
@@ -0,0 +1,57 @@
/*
* 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.
*/
#ifndef _CI1302_H_
#define _CI1302_H_
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
void voiceControlPowerOn(void);
void voiceControlPowerOff(void);
void voiceSerialPutc(char c);
int voiceGetByte(uint8_t* byte);
int GetVoiceInput(uint8_t *rxchar);
int dbgGetByte(uint8_t* byte);
void voiceSerialPutstr(uint8_t* byte, uint32_t len);
#ifdef __cplusplus
}
#endif
#ifdef __cplusplus
extern bool VoiceGearStatus;
extern uint8_t VoiceFlapStatus;
extern bool VoicePowerStatus;
extern bool VoiceRunStatus;
extern bool MotionControlStatus;
void processUpdataInput(void);
void processVoiceInput(void);
void voiceControlInit(void);
#endif
#endif
@@ -0,0 +1,65 @@
/*
* Copyright (C) EdgeTX
*
* CI1302 voice control hooks for shared EdgeTX code (VOICE_CONTROL_SENSOR).
* Implementation lives in drivers/CI1302.cpp (CI1302_SENSOR targets).
*/
#pragma once
#include <stdint.h>
#include <stddef.h>
#if defined(VOICE_CONTROL_SENSOR)
#include "edgetx_constants.h"
#include "edgetx_types.h"
#include "hal/switch_driver.h"
#define VOICE_SWITCH_COUNT 2
// --- shared-code integration hooks ---
void CI1302_voiceIntegrationPer10ms();
void CI1302_voiceIntegrationOnFlightReset();
bool CI1302_voiceIntegrationMixSrcValue(mixsrc_t i, getvalue_t* val);
bool CI1302_voiceIntegrationSourceAvailable(mixsrc_t i);
const char* CI1302_voiceIntegrationMixSrcName(mixsrc_t idx);
bool CI1302_voiceIntegrationMixSrcParseYaml(const char* val, uint8_t val_len, uint32_t* out);
bool CI1302_voiceIntegrationMixSrcWriteYaml(uint32_t val, const char** str);
bool CI1302_voiceIntegrationSwitchSrcParseYaml(const char* val, uint8_t val_len, int32_t* ival);
bool CI1302_voiceIntegrationIsSwitchAvailable(int swtch, int context, bool* available);
bool CI1302_voiceIntegrationIsSwitchSwitchAvailable(int swtch, bool* available);
// --- virtual voice switches (VGR / VFL) ---
uint8_t CI1302_voiceSwitchExtraCount();
void CI1302_voiceSwitchSetDefaults();
bool CI1302_voiceSwitchTryGetPosition(uint8_t idx, SwitchHwPos* pos);
const char* CI1302_voiceSwitchTryGetName(uint8_t idx);
int8_t CI1302_voiceSwitchTryGetIndexFromName(const char* name);
bool CI1302_voiceSwitchTryGetHwType(uint8_t idx, SwitchHwType* type);
bool CI1302_voiceSwitchIsIndex(uint8_t idx);
bool CI1302_voiceSwitchIsPositionAvailable(uint8_t idx, uint8_t position);
bool CI1302_voiceSwitchTryIsAvailable(int swtch, int context, bool* available);
bool CI1302_voiceSwitchTryParseYamlSrc(const char* val, uint8_t val_len, int32_t* ival);
void CI1302_voiceSwitchInitBootGuard();
bool CI1302_voiceSwitchBootGuardActive();
bool CI1302_voiceSwitchSuppressMovedAudio(uint8_t idx, bool startup);
bool CI1302_voiceSwitchIsSwitchSource(swsrc_t swtch);
bool CI1302_voiceSwitchShouldSuppressFunctionAudio(swsrc_t swtch, bool risingEdge);
// --- motion control (mixer hook) ---
void CI1302_voiceMotionControlApplyToInputs(uint8_t pots_offset);
#endif
+15
View File
@@ -46,6 +46,10 @@
#include "input_mapping.h"
#include "trainer.h"
#if defined(VOICE_CONTROL_SENSOR)
#include "drivers/CI1302_voice_integration.h"
#endif
#include "tasks.h"
#include "tasks/mixer_task.h"
#include "os/async.h"
@@ -231,6 +235,9 @@ void per10ms()
#if defined(CSD203_SENSOR) && !defined(SIMU)
readCSD203();
#endif
#if defined(VOICE_CONTROL_SENSOR) && !defined(SIMU)
CI1302_voiceIntegrationPer10ms();
#endif
telemetryInterrupt10ms();
@@ -312,6 +319,10 @@ void generalDefaultSwitches()
#endif
#endif
}
#if defined(VOICE_CONTROL_SENSOR)
CI1302_voiceSwitchSetDefaults();
#endif
}
void generalDefaultUILanguage()
@@ -1139,6 +1150,10 @@ void flightReset(uint8_t check)
START_SILENCE_PERIOD();
#if defined(VOICE_CONTROL_SENSOR) && !defined(SIMU)
CI1302_voiceIntegrationOnFlightReset();
#endif
RESET_THR_TRACE();
logicalSwitchesReset();
+5
View File
@@ -528,6 +528,11 @@ enum AUDIO_SOUNDS {
AU_TIMER2_ELAPSED,
AU_TIMER3_ELAPSED,
#if defined(VOICE_CONTROL_SENSOR)
AU_HELLORADIO,
AU_OKAY,
#endif
AU_SPECIAL_SOUND_FIRST,
AU_SPECIAL_SOUND_BEEP1 = AU_SPECIAL_SOUND_FIRST,
AU_SPECIAL_SOUND_BEEP2,
+16
View File
@@ -23,6 +23,10 @@
#include "switches.h"
#include "tasks/mixer_task.h"
#if defined(VOICE_CONTROL_SENSOR)
#include "drivers/CI1302_voice_integration.h"
#endif
#include "hal/audio_driver.h"
#include "os/time.h"
@@ -343,7 +347,13 @@ void evalFunctions(CustomFunctionData * functions, CustomFunctionsContext & func
#endif
{
if (isRepeatDelayElapsed(functions, functionsContext, i)) {
#if defined(VOICE_CONTROL_SENSOR) && !defined(SIMU)
bool risingEdge = !(functionsContext.activeSwitches & ((MASK_CFN_TYPE)1 << i));
if (!CI1302_voiceSwitchShouldSuppressFunctionAudio(swtch, risingEdge) &&
!IS_PLAYING(PLAY_INDEX)) {
#else
if (!IS_PLAYING(PLAY_INDEX)) {
#endif
if (CFN_FUNC(cfn) == FUNC_PLAY_SOUND) {
AUDIO_PLAY(AU_SPECIAL_SOUND_FIRST + CFN_PARAM(cfn));
} else if (CFN_FUNC(cfn) == FUNC_PLAY_VALUE) {
@@ -365,7 +375,13 @@ void evalFunctions(CustomFunctionData * functions, CustomFunctionsContext & func
case FUNC_BACKGND_MUSIC:
if (!(newActiveFunctions & (1 << FUNCTION_BACKGND_MUSIC))) {
newActiveFunctions |= (1 << FUNCTION_BACKGND_MUSIC);
#if defined(VOICE_CONTROL_SENSOR) && !defined(SIMU)
bool risingEdge = !(functionsContext.activeSwitches & ((MASK_CFN_TYPE)1 << i));
if (!CI1302_voiceSwitchShouldSuppressFunctionAudio(swtch, risingEdge) &&
!IS_PLAYING(PLAY_INDEX)) {
#else
if (!IS_PLAYING(PLAY_INDEX)) {
#endif
playCustomFunctionFile(cfn, PLAY_INDEX);
}
}
+37
View File
@@ -175,6 +175,37 @@ static void keyboardDriverRead(lv_indev_drv_t *drv, lv_indev_data_t *data)
}
}
#if defined(VOICE_CONTROL_SENSOR)
static bool voiceEnterPending = false;
static void lvglInjectKeyboardEnter()
{
if (!keyboardDevice) return;
auto obj = get_focus_obj(keyboardDevice);
if (obj) {
lv_group_t* g = keyboardDevice->group;
if (g && !lv_obj_has_state(obj, LV_STATE_DISABLED)) {
// Same sequence as lv_indev keypad Enter (press then release).
lv_group_send_data(g, LV_KEY_ENTER);
lv_event_send(obj, LV_EVENT_PRESSED, keyboardDevice);
lv_event_send(obj, LV_EVENT_RELEASED, keyboardDevice);
lv_event_send(obj, LV_EVENT_SHORT_CLICKED, keyboardDevice);
lv_event_send(obj, LV_EVENT_CLICKED, keyboardDevice);
}
return;
}
auto w = Window::topWindow();
dispatch_kb_event(w, EVT_KEY_BREAK(KEY_ENTER));
}
void lvglRequestKeyboardEnter()
{
voiceEnterPending = true;
}
#endif
extern "C" void touchDriverRead(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
#if defined(HARDWARE_TOUCH)
@@ -370,6 +401,12 @@ void LvglWrapper::run()
if (!updating) {
// Normal UI loop - call lgvl timer handler
updating = true;
#if defined(VOICE_CONTROL_SENSOR)
if (voiceEnterPending) {
voiceEnterPending = false;
lvglInjectKeyboardEnter();
}
#endif
lv_timer_handler();
updating = false;
} else {
+4
View File
@@ -37,3 +37,7 @@ class LvglWrapper
LvglWrapper();
~LvglWrapper() {}
};
#if defined(VOICE_CONTROL_SENSOR)
void lvglRequestKeyboardEnter();
#endif
@@ -54,6 +54,9 @@ class SourceChoiceMenuToolbar : public MenuToolbar
[=](int16_t index) {
#if defined(LUMINOSITY_SENSOR)
if (index == MIXSRC_LIGHT) return true;
#endif
#if defined(VOICE_CONTROL_SENSOR)
if (index >= MIXSRC_VGR && index <= MIXSRC_LAST_VOICE) return true;
#endif
return (index >= MIXSRC_MIN && index <= MIXSRC_MAX) ||
(index >= MIXSRC_TX_VOLTAGE && index <= MIXSRC_LAST_TIMER);
+35 -1
View File
@@ -26,6 +26,9 @@
#include "edgetx.h"
#include "switches.h"
#if defined(VOICE_CONTROL_SENSOR)
#include "drivers/CI1302_voice_integration.h"
#endif
#include "mixes.h"
#include "os/sleep.h"
@@ -206,9 +209,19 @@ static bool isSourceTrimAvailable(int source) {
}
static bool isSourceSwitchAvailable(int source) {
#if defined(VOICE_CONTROL_SENSOR)
// VGR/VFL have their own dedicated sources
if (CI1302_voiceSwitchIsIndex(source)) return false;
#endif
return SWITCH_EXISTS(source);
}
#if defined(VOICE_CONTROL_SENSOR)
static bool isSourceVoiceAvailable(int source) {
return CI1302_voiceIntegrationSourceAvailable(MIXSRC_VGR + source);
}
#endif
#if defined(FUNCTION_SWITCHES)
static bool isSourceFuncSwitchAvailable(int source) {
return getSwitchCountInFSGroup(source + 1) > 0;
@@ -281,6 +294,9 @@ static struct sourceAvailableCheck sourceChecks[] = {
{ MIXSRC_FIRST_CH, MIXSRC_LAST_CH, SRC_CHANNEL_ALL, sourceIsAvailable },
{ MIXSRC_FIRST_GVAR, MIXSRC_LAST_GVAR, SRC_GVAR, isSourceGvarAvailable },
{ MIXSRC_TX_VOLTAGE, MIXSRC_TX_GPS, SRC_TX, sourceIsAvailable },
#if defined(VOICE_CONTROL_SENSOR)
{ MIXSRC_VGR, MIXSRC_LAST_VOICE, SRC_VOICE, isSourceVoiceAvailable },
#endif
{ MIXSRC_FIRST_TIMER, MIXSRC_LAST_TIMER, SRC_TIMER, isSourceTimerAvailable },
{ MIXSRC_FIRST_TELEM, MIXSRC_LAST_TELEM, SRC_TELEM, isSourceTelemAvailable },
{ MIXSRC_NONE, MIXSRC_NONE, SRC_NONE, sourceIsAvailable },
@@ -307,7 +323,11 @@ bool checkSourceAvailable(int source, uint32_t sourceTypes)
bool isSourceAvailable(int source)
{
return checkSourceAvailable(source,
SRC_COMMON | SRC_INPUT | SRC_LUA | SRC_HELI | SRC_CHANNEL | SRC_TX | SRC_TIMER | SRC_TELEM | SRC_NONE
SRC_COMMON | SRC_INPUT | SRC_LUA | SRC_HELI | SRC_CHANNEL | SRC_TX | SRC_TIMER | SRC_TELEM
#if defined(VOICE_CONTROL_SENSOR)
| SRC_VOICE
#endif
| SRC_NONE
);
}
@@ -341,6 +361,13 @@ bool isSwitchAvailable(int swtch, SwitchContext context)
return false;
}
#if defined(VOICE_CONTROL_SENSOR)
bool voiceAvailable;
if (CI1302_voiceIntegrationIsSwitchAvailable(swtch, (int)context, &voiceAvailable)) {
return voiceAvailable;
}
#endif
if (!SWITCH_EXISTS(swinfo.quot)) {
return false;
}
@@ -418,6 +445,13 @@ static bool isSwitchSwitchAvailable(int swtch, bool invert) {
return false;
}
#if defined(VOICE_CONTROL_SENSOR)
bool voiceAvailable;
if (CI1302_voiceIntegrationIsSwitchSwitchAvailable(swtch, &voiceAvailable)) {
return voiceAvailable;
}
#endif
if (!SWITCH_EXISTS(swinfo.quot)) {
return false;
}
+43 -1
View File
@@ -27,6 +27,9 @@
#include "dataconstants.h"
#include "edgetx_helpers.h"
#include "edgetx.h"
#if defined(VOICE_CONTROL_SENSOR)
#include "drivers/CI1302_voice_integration.h"
#endif
// Board API
void boardInitSwitches();
@@ -85,7 +88,11 @@ uint8_t switchGetMaxSwitches()
uint8_t switchGetMaxAllSwitches()
{
return boardGetMaxSwitches() + MAX_FLEX_SWITCHES;
uint8_t n = boardGetMaxSwitches() + MAX_FLEX_SWITCHES;
#if defined(VOICE_CONTROL_SENSOR)
n += CI1302_voiceSwitchExtraCount();
#endif
return n;
}
uint32_t switchState(uint8_t pos_idx)
@@ -99,6 +106,13 @@ uint32_t switchState(uint8_t pos_idx)
SwitchHwPos switchGetPosition(uint8_t sw_idx)
{
#if defined(VOICE_CONTROL_SENSOR)
SwitchHwPos pos;
if (CI1302_voiceSwitchTryGetPosition(sw_idx, &pos)) {
return pos;
}
#endif
auto idx = (int)sw_idx;
auto max_switches = switchGetMaxSwitches();
if (idx < max_switches) {
@@ -137,6 +151,12 @@ static_assert(DIM(_flex_sw_canon_names) >= MAX_FLEX_SWITCHES,
const char* switchGetDefaultName(uint8_t sw_idx)
{
#if defined(VOICE_CONTROL_SENSOR)
if (const char* name = CI1302_voiceSwitchTryGetName(sw_idx)) {
return name;
}
#endif
auto idx = (int)sw_idx;
auto max_switches = switchGetMaxSwitches();
if (idx < max_switches) {
@@ -161,11 +181,27 @@ int8_t switchGetIndexFromName(const char* name)
if (strcmp(name, boardSwitchGetName(i)) == 0)
return i;
#if defined(VOICE_CONTROL_SENSOR)
{
int8_t idx = CI1302_voiceSwitchTryGetIndexFromName(name);
if (idx >= 0) {
return idx;
}
}
#endif
return -1;
}
SwitchHwType switchGetHwType(uint8_t sw_idx)
{
#if defined(VOICE_CONTROL_SENSOR)
SwitchHwType type;
if (CI1302_voiceSwitchTryGetHwType(sw_idx, &type)) {
return type;
}
#endif
auto idx = (int)sw_idx;
auto max_switches = switchGetMaxSwitches();
if (idx < max_switches) {
@@ -183,7 +219,13 @@ SwitchHwType switchGetHwType(uint8_t sw_idx)
bool switchIsFlex(uint8_t idx)
{
auto max_switches = switchGetMaxSwitches();
#if defined(VOICE_CONTROL_SENSOR)
if (idx < max_switches) return false;
idx -= max_switches;
return idx < MAX_FLEX_SWITCHES;
#else
return idx >= max_switches && idx < switchGetMaxAllSwitches();
#endif
}
void switchConfigFlex_raw(uint8_t idx, int8_t channel)
+17
View File
@@ -22,6 +22,9 @@
#include "edgetx.h"
#include "edgetx_types.h"
#include "timers.h"
#if defined(VOICE_CONTROL_SENSOR)
#include "drivers/CI1302_voice_integration.h"
#endif
#include "switches.h"
#include "input_mapping.h"
#include "mixes.h"
@@ -403,6 +406,16 @@ getvalue_t _getValue(mixsrc_t i, bool* valid)
}
#endif
#if defined(VOICE_CONTROL_SENSOR)
else if (i == MIXSRC_VGR || i == MIXSRC_VFL) {
getvalue_t voiceVal = 0;
// false also means "configured as None" here, not just "unknown source".
if (!CI1302_voiceIntegrationMixSrcValue(i, &voiceVal) && valid != nullptr)
*valid = false;
return voiceVal;
}
#endif
else if (i == MIXSRC_MIN) {
return -RESX;
}
@@ -642,6 +655,10 @@ void evalInputs(uint8_t mode)
}
}
#if defined(VOICE_CONTROL_SENSOR) && !defined(SIMU)
CI1302_voiceMotionControlApplyToInputs(pots_offset);
#endif
// EXPOs
applyExpos(anas, mode);
@@ -34,6 +34,10 @@
#include "hal/adc_driver.h"
#include "hal/audio_driver.h"
#if defined(VOICE_CONTROL_SENSOR)
#include "drivers/CI1302_voice_integration.h"
#endif
#if defined(COLORLCD)
#include "radio_tools.h"
#endif
@@ -291,6 +295,15 @@ static uint32_t r_mixSrcRaw(const YamlNode* node, const char* val, uint8_t val_l
return MIXSRC_FIRST_TRIM + (val[1] - '1');
}
#if defined(VOICE_CONTROL_SENSOR)
{
uint32_t voiceSrc;
if (CI1302_voiceIntegrationMixSrcParseYaml(val, val_len, &voiceSrc)) {
return voiceSrc;
}
}
#endif
auto idx = analogLookupCanonicalIdx(ADC_INPUT_MAIN, val, val_len);
if (idx >= 0) return idx + MIXSRC_FIRST_STICK;
@@ -407,6 +420,11 @@ static bool w_mixSrcRaw(const YamlNode* node, uint32_t val, yaml_writer_func wf,
return false;
str = closing_parenthesis;
}
#if defined(VOICE_CONTROL_SENSOR)
else if (CI1302_voiceIntegrationMixSrcWriteYaml(val, &str)) {
// handled
}
#endif
else if (val >= MIXSRC_FIRST_TIMER
&& val <= MIXSRC_LAST_TIMER) {
if (!wf(opaque, "Tmr", 3)) return false;
@@ -1395,6 +1413,11 @@ static uint32_t r_swtchSrc(const YamlNode* node, const char* val, uint8_t val_le
ival += SWSRC_FIRST_SWITCH;
}
#if defined(VOICE_CONTROL_SENSOR)
else if (CI1302_voiceIntegrationSwitchSrcParseYaml(val, val_len, &ival)) {
// parsed
}
#endif
else if (val_len > 3
&& val[0] == '6'
&& val[1] == 'P'
+9
View File
@@ -29,6 +29,9 @@
#include "hal/switch_driver.h"
#include "edgetx.h"
#include "switches.h"
#if defined(VOICE_CONTROL_SENSOR)
#include "drivers/CI1302_voice_integration.h"
#endif
static char _static_str_buffer[32];
static const char s_charTab[] = "_-.,";
@@ -744,6 +747,12 @@ char *getSourceString(char (&destRef)[L], mixsrc_t idx, bool defaultOnly)
else if (idx == MIXSRC_LIGHT) {
strncpy(dest, STR_SRC_LIGHT, dest_len - 1);
}
#endif
#if defined(VOICE_CONTROL_SENSOR)
else if (idx >= MIXSRC_VGR && idx <= MIXSRC_LAST_VOICE) {
const char* voiceName = CI1302_voiceIntegrationMixSrcName(idx);
strncpy(dest, voiceName ? voiceName : "", dest_len - 1);
}
#endif
else if (idx <= MIXSRC_LAST_HELI) {
idx -= MIXSRC_FIRST_HELI;
+13 -2
View File
@@ -28,6 +28,9 @@
#include "edgetx_constants.h"
#include "os/sleep.h"
#include "switches.h"
#if defined(VOICE_CONTROL_SENSOR)
#include "drivers/CI1302_voice_integration.h"
#endif
#include "input_mapping.h"
#include "inactivity_timer.h"
#include "tasks/mixer_task.h"
@@ -303,7 +306,12 @@ int switchLookupIdx(char c)
int switchLookupIdx(const char* name, size_t len)
{
if (len < 2 || (name[0] != 'S' && name[0] != 'F')) return -1;
if (len < 2) return -1;
if (name[0] != 'S' && name[0] != 'F'
#if defined(VOICE_CONTROL_SENSOR)
&& name[0] != 'V' // VGR/VFL
#endif
) return -1;
auto max_switches = switchGetMaxAllSwitches();
for (int i = 0; i < max_switches; i++) {
@@ -380,7 +388,10 @@ static uint64_t checkSwitchPosition(uint8_t idx, bool startup)
}
if (!(switchesPos & result)) {
PLAY_SWITCH_MOVED(index);
#if defined(VOICE_CONTROL_SENSOR) && !defined(SIMU)
if (!CI1302_voiceSwitchSuppressMovedAudio(idx, startup))
#endif
PLAY_SWITCH_MOVED(index);
}
return result;
+9
View File
@@ -116,6 +116,7 @@ if (PCB STREQUAL X10)
set(FLEXSW "2" CACHE STRING "Max flex inputs usable as switches")
set(AUX2_SERIAL OFF)
set(CSD203_SENSOR ON)
set(CI1302_SENSOR ON)
set(FLYSKY_GIMBAL ON)
add_definitions(-DIMU_ICM42627)
add_definitions(-DIMU_SC7U22)
@@ -350,6 +351,14 @@ if (MULTIMODULE)
add_definitions(-DMULTI_PROTOLIST)
endif()
if(CI1302_SENSOR)
# Via SRC (not target_sources(board ...)), same as io/frsky_firmware_update.cpp
# below: made available to SIMU/WASM builds, not just firmware. Live-sensor
# call sites are SIMU-guarded individually.
set(SRC ${SRC} drivers/CI1302.cpp)
add_definitions(-DVOICE_CONTROL_SENSOR)
endif()
set(SRC ${SRC}
io/frsky_firmware_update.cpp
io/multi_firmware_update.cpp
+8
View File
@@ -115,6 +115,10 @@ void boardBLInit()
#if !defined(BOOT)
#include "edgetx.h"
#if defined(VOICE_CONTROL_SENSOR)
#include "drivers/CI1302.h"
#endif
#if defined(PCBX12S)
static void audio_set_rst_pin(bool set)
{
@@ -239,6 +243,10 @@ void boardInit()
bluetoothInit(BLUETOOTH_DEFAULT_BAUDRATE, true);
#endif
#if defined(VOICE_CONTROL_SENSOR)
voiceControlInit();
#endif
#if defined(VIDEO_SWITCH)
videoSwitchInit();
#endif
+14
View File
@@ -485,6 +485,20 @@
#define FLYSKY_HALL_DMA_Stream_TX LL_DMA_STREAM_4
#endif
#if defined(VOICE_CONTROL_SENSOR)
#if defined(RADIO_V16)
#define VOICE_CTRL_BAUD 115200
#define VOICE_CONTROL_USART UART7
#define VOICE_CONTROL_USART_IRQn UART7_IRQn
#define VOICE_CONTROL_GPIO GPIOF
#define VOICE_CONTROL_TX_GPIO GPIO_PIN(GPIOF, 7) // PF.7
#define VOICE_CONTROL_RX_GPIO GPIO_PIN(GPIOF, 6) // PF.6
#define VOICE_CONTROL_PWR_GPIO GPIO_PIN(GPIOH, 13) // PH.13 1=ON, 0=OFF
#else
#error VOICE_CONTROL_SENSOR is only supported on RADIO_V16
#endif
#endif
// Internal Module
#define INTMODULE_PWR_GPIO GPIO_PIN(GPIOA, 8) // PA.08
#define INTMODULE_TX_GPIO GPIO_PIN(GPIOB, 6) // PB.06
+7 -1
View File
@@ -40,7 +40,13 @@ uint16_t getLuxSensorValue()
uint16_t getBatteryVoltage()
{
if (adcGetMaxInputs(ADC_INPUT_VBAT) < 1) return 0;
if (adcGetMaxInputs(ADC_INPUT_VBAT) < 1) {
// Battery is not an ADC input on this board (e.g. read from an I2C
// sensor on hardware), so report the same default as simu_start_conversion
uint16_t vBatWarn = BATTERY_WARN;
if (g_eeGeneral.vBatWarn > 0) vBatWarn = g_eeGeneral.vBatWarn;
return (vBatWarn + 5) * 10;
}
return anaIn(adcGetInputOffset(ADC_INPUT_VBAT));
}