diff --git a/companion/src/firmwares/boardjson.cpp b/companion/src/firmwares/boardjson.cpp index 48741b477..ba43752fd 100644 --- a/companion/src/firmwares/boardjson.cpp +++ b/companion/src/firmwares/boardjson.cpp @@ -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 diff --git a/companion/src/firmwares/edgetx/yaml_rawswitch.cpp b/companion/src/firmwares/edgetx/yaml_rawswitch.cpp index da3ad5e11..9f1d3c561 100644 --- a/companion/src/firmwares/edgetx/yaml_rawswitch.cpp +++ b/companion/src/firmwares/edgetx/yaml_rawswitch.cpp @@ -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); diff --git a/companion/src/firmwares/rawsource.cpp b/companion/src/firmwares/rawsource.cpp index 552c5e3a0..ec562ab99 100644 --- a/companion/src/firmwares/rawsource.cpp +++ b/companion/src/firmwares/rawsource.cpp @@ -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"}, }); diff --git a/companion/src/tests/voice_control_yaml_test.cpp b/companion/src/tests/voice_control_yaml_test.cpp new file mode 100644 index 000000000..5c1bc6414 --- /dev/null +++ b/companion/src/tests/voice_control_yaml_test.cpp @@ -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 + +#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); +} diff --git a/radio/src/audio.cpp b/radio/src/audio.cpp index aea6d7abd..330e4487a 100644 --- a/radio/src/audio.cpp +++ b/radio/src/audio.cpp @@ -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 = diff --git a/radio/src/audio.h b/radio/src/audio.h index 0fc1eb030..36d9d77cd 100644 --- a/radio/src/audio.h +++ b/radio/src/audio.h @@ -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) diff --git a/radio/src/dataconstants.h b/radio/src/dataconstants.h index df632949c..c956fc329 100644 --- a/radio/src/dataconstants.h +++ b/radio/src/dataconstants.h @@ -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, }; diff --git a/radio/src/drivers/CI1302.cpp b/radio/src/drivers/CI1302.cpp new file mode 100644 index 000000000..40377a820 --- /dev/null +++ b/radio/src/drivers/CI1302.cpp @@ -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 + +#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(-RESX, stick + deltaX, RESX); + } else if (role == STICK_ROLE_ELE) { + calibratedAnalogs[phys] = + limit(-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 diff --git a/radio/src/drivers/CI1302.h b/radio/src/drivers/CI1302.h new file mode 100644 index 000000000..86fcd22c9 --- /dev/null +++ b/radio/src/drivers/CI1302.h @@ -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 + +#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 diff --git a/radio/src/drivers/CI1302_voice_integration.h b/radio/src/drivers/CI1302_voice_integration.h new file mode 100644 index 000000000..6ca29ef79 --- /dev/null +++ b/radio/src/drivers/CI1302_voice_integration.h @@ -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 +#include + +#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 diff --git a/radio/src/edgetx.cpp b/radio/src/edgetx.cpp index 26f16a727..95490627b 100644 --- a/radio/src/edgetx.cpp +++ b/radio/src/edgetx.cpp @@ -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(); diff --git a/radio/src/edgetx.h b/radio/src/edgetx.h index 52d86a23d..3832a1078 100644 --- a/radio/src/edgetx.h +++ b/radio/src/edgetx.h @@ -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, diff --git a/radio/src/functions.cpp b/radio/src/functions.cpp index 2515eea5a..57cf198f4 100644 --- a/radio/src/functions.cpp +++ b/radio/src/functions.cpp @@ -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); } } diff --git a/radio/src/gui/colorlcd/LvglWrapper.cpp b/radio/src/gui/colorlcd/LvglWrapper.cpp index d918b5250..400e3b01f 100644 --- a/radio/src/gui/colorlcd/LvglWrapper.cpp +++ b/radio/src/gui/colorlcd/LvglWrapper.cpp @@ -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 { diff --git a/radio/src/gui/colorlcd/LvglWrapper.h b/radio/src/gui/colorlcd/LvglWrapper.h index 05cb6a4b5..7f98e4cde 100644 --- a/radio/src/gui/colorlcd/LvglWrapper.h +++ b/radio/src/gui/colorlcd/LvglWrapper.h @@ -37,3 +37,7 @@ class LvglWrapper LvglWrapper(); ~LvglWrapper() {} }; + +#if defined(VOICE_CONTROL_SENSOR) +void lvglRequestKeyboardEnter(); +#endif diff --git a/radio/src/gui/colorlcd/controls/sourcechoice.cpp b/radio/src/gui/colorlcd/controls/sourcechoice.cpp index 7d809635b..fa2b63ecd 100644 --- a/radio/src/gui/colorlcd/controls/sourcechoice.cpp +++ b/radio/src/gui/colorlcd/controls/sourcechoice.cpp @@ -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); diff --git a/radio/src/gui/gui_common.cpp b/radio/src/gui/gui_common.cpp index 21c800231..b9a49f2a9 100644 --- a/radio/src/gui/gui_common.cpp +++ b/radio/src/gui/gui_common.cpp @@ -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; } diff --git a/radio/src/hal/switch_driver.cpp b/radio/src/hal/switch_driver.cpp index 384d99042..9dcfe650c 100644 --- a/radio/src/hal/switch_driver.cpp +++ b/radio/src/hal/switch_driver.cpp @@ -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) diff --git a/radio/src/mixer.cpp b/radio/src/mixer.cpp index 38173392f..c6010d317 100644 --- a/radio/src/mixer.cpp +++ b/radio/src/mixer.cpp @@ -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); diff --git a/radio/src/storage/yaml/yaml_datastructs_funcs.cpp b/radio/src/storage/yaml/yaml_datastructs_funcs.cpp index ba41d8139..3ebf07d21 100644 --- a/radio/src/storage/yaml/yaml_datastructs_funcs.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_funcs.cpp @@ -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' diff --git a/radio/src/strhelpers.cpp b/radio/src/strhelpers.cpp index bf20484c1..9492d87ab 100644 --- a/radio/src/strhelpers.cpp +++ b/radio/src/strhelpers.cpp @@ -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; diff --git a/radio/src/switches.cpp b/radio/src/switches.cpp index be3bbd848..90d407cd6 100644 --- a/radio/src/switches.cpp +++ b/radio/src/switches.cpp @@ -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; diff --git a/radio/src/targets/horus/CMakeLists.txt b/radio/src/targets/horus/CMakeLists.txt index b85ab0039..5df38dffd 100644 --- a/radio/src/targets/horus/CMakeLists.txt +++ b/radio/src/targets/horus/CMakeLists.txt @@ -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 diff --git a/radio/src/targets/horus/board.cpp b/radio/src/targets/horus/board.cpp index 49fa1b5d8..7c0536e91 100644 --- a/radio/src/targets/horus/board.cpp +++ b/radio/src/targets/horus/board.cpp @@ -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 diff --git a/radio/src/targets/horus/hal.h b/radio/src/targets/horus/hal.h index 38bb95fcf..10118d352 100644 --- a/radio/src/targets/horus/hal.h +++ b/radio/src/targets/horus/hal.h @@ -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 diff --git a/radio/src/targets/simu/adc_driver.cpp b/radio/src/targets/simu/adc_driver.cpp index 7ab04c035..09a15dce4 100644 --- a/radio/src/targets/simu/adc_driver.cpp +++ b/radio/src/targets/simu/adc_driver.cpp @@ -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)); }