diff --git a/.trunk/.gitignore b/.trunk/.gitignore
index 8130ba6d1..695b51906 100644
--- a/.trunk/.gitignore
+++ b/.trunk/.gitignore
@@ -5,4 +5,4 @@
plugins
user_trunk.yaml
user.yaml
-shims
+tools
diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml
index 8ab9166fb..7e81046a7 100644
--- a/.trunk/trunk.yaml
+++ b/.trunk/trunk.yaml
@@ -1,37 +1,42 @@
version: 0.1
cli:
- version: 1.10.0
+ version: 1.13.0
plugins:
sources:
- id: trunk
- ref: v0.0.17
+ ref: v1.1.1
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- - taplo@0.7.0
- - ruff@0.0.265
+ - bandit@1.7.5
+ - checkov@2.4.1
+ - terrascan@1.18.3
+ - trivy@0.44.1
+ - trufflehog@3.48.0
+ - taplo@0.8.1
+ - ruff@0.0.284
- yamllint@1.32.0
- isort@5.12.0
- - markdownlint@0.34.0
+ - markdownlint@0.35.0
- oxipng@8.0.0
- svgo@3.0.2
- - actionlint@1.6.24
- - flake8@6.0.0
+ - actionlint@1.6.25
+ - flake8@6.1.0
- hadolint@2.12.0
- - shfmt@3.5.0
+ - shfmt@3.6.0
- shellcheck@0.9.0
- - black@23.3.0
+ - black@23.7.0
- git-diff-check
- - gitleaks@8.16.3
- - clang-format@14.0.0
- - prettier@2.8.8
+ - gitleaks@8.17.0
+ - clang-format@16.0.3
+ - prettier@3.0.2
disabled:
- - taplo@0.7.0
+ - taplo@0.8.1
- shellcheck@0.9.0
- - shfmt@3.5.0
+ - shfmt@3.6.0
- oxipng@8.0.0
- actionlint@1.6.22
- - markdownlint@0.34.0
+ - markdownlint@0.35.0
- hadolint@2.12.0
- svgo@3.0.2
runtimes:
diff --git a/protobufs b/protobufs
index dc28ae3d1..468ff2e24 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit dc28ae3d128b76707c0b87b6f3b2514c7f8514bd
+Subproject commit 468ff2e2457d7534a907af0a21bdede9f4042cb7
diff --git a/src/Power.cpp b/src/Power.cpp
index 533286fe9..dd9f70130 100644
--- a/src/Power.cpp
+++ b/src/Power.cpp
@@ -16,8 +16,8 @@
#include "buzz/buzz.h"
#include "configuration.h"
#include "main.h"
+#include "meshUtils.h"
#include "sleep.h"
-#include "utils.h"
#ifdef DEBUG_HEAP_MQTT
#include "mqtt/MQTT.h"
@@ -221,10 +221,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
/**
* return true if there is a battery installed in this unit
*/
- virtual bool isBatteryConnect() override
- {
- return getBatteryPercent() != -1;
- }
+ virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
/// If we see a battery voltage higher than physics allows - assume charger is pumping
/// in power
@@ -245,10 +242,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
/// Assume charging if we have a battery and external power is connected.
/// we can't be smart enough to say 'full'?
- virtual bool isCharging() override
- {
- return isBatteryConnect() && isVbusIn();
- }
+ virtual bool isCharging() override { return isBatteryConnect() && isVbusIn(); }
private:
/// If we see a battery voltage higher than physics allows - assume charger is pumping
diff --git a/src/configuration.h b/src/configuration.h
index fb96430bc..e116854c2 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -101,6 +101,7 @@ along with this program. If not, see .
// I2C Keyboards (M5Stack, RAK14004, T-Deck)
#define CARDKB_ADDR 0x5F
#define TDECK_KB_ADDR 0x55
+#define BBQ10_KB_ADDR 0x1F
// -----------------------------------------------------------------------------
// SENSOR
diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp
index 996bdf62a..bf206c190 100644
--- a/src/detect/ScanI2C.cpp
+++ b/src/detect/ScanI2C.cpp
@@ -30,8 +30,8 @@ ScanI2C::FoundDevice ScanI2C::firstRTC() const
ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
{
- ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, RAK14004};
- return firstOfOrNONE(3, types);
+ ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004};
+ return firstOfOrNONE(4, types);
}
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h
index 418a6bf5e..15d9b1342 100644
--- a/src/detect/ScanI2C.h
+++ b/src/detect/ScanI2C.h
@@ -17,6 +17,7 @@ class ScanI2C
RTC_PCF8563,
CARDKB,
TDECKKB,
+ BBQ10KB,
RAK14004,
PMU_AXP192_AXP2101,
BME_680,
diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp
index d8bddee30..f4f490085 100644
--- a/src/detect/ScanI2CTwoWire.cpp
+++ b/src/detect/ScanI2CTwoWire.cpp
@@ -213,6 +213,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port)
break;
SCAN_SIMPLE_CASE(TDECK_KB_ADDR, TDECKKB, "T-Deck keyboard found\n");
+ SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10 keyboard found\n");
SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "st7567 display found\n");
#ifdef HAS_NCP5623
SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623 RGB LED found\n");
diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp
index 27fe36fe5..ab5156fab 100644
--- a/src/gps/GPS.cpp
+++ b/src/gps/GPS.cpp
@@ -188,8 +188,8 @@ bool GPS::setupGPS()
config.position.tx_gpio = GPS_TX_PIN;
#endif
-//#define BAUD_RATE 115200
-// ESP32 has a special set of parameters vs other arduino ports
+// #define BAUD_RATE 115200
+// ESP32 has a special set of parameters vs other arduino ports
#if defined(ARCH_ESP32)
if (config.position.rx_gpio) {
LOG_DEBUG("Using GPIO%d for GPS RX\n", config.position.rx_gpio);
@@ -267,7 +267,6 @@ bool GPS::setupGPS()
LOG_INFO("GNSS configured for GPS+SBAS+GLONASS. Pause for 0.75s before sending next command.\n");
// Documentation say, we need wait atleast 0.5s after reconfiguration of GNSS module, before sending next commands
delay(750);
- return true;
}
// Enable interference resistance, because we are using LoRa, WiFi and Bluetooth on same board,
@@ -535,7 +534,8 @@ bool GPS::setupGPS()
_serial_gps->write(UBX_CFG_PMS, sizeof(UBX_CFG_PMS));
if (!getACK(0x06, 0x86)) {
LOG_WARN("Unable to enable powersaving for GPS.\n");
- return true;
+ // T-beam doesn't support this mode.
+ // Don't bail from function early.
}
// We need save configuration to flash to make our config changes persistent
@@ -675,6 +675,7 @@ void GPS::setAwake(bool on)
if (isAwake != on) {
LOG_DEBUG("WANT GPS=%d\n", on);
if (on) {
+ clearBuffer(); // drop any old data waiting in the buffer
lastWakeStartMsec = millis();
wake();
} else {
@@ -858,7 +859,7 @@ GnssModel_t GPS::probe()
{
memset(&info, 0, sizeof(struct uBloxGnssModelInfo));
// return immediately if the model is set by the variant.h file
-//#ifdef GPS_UBLOX (unless it's a ublox, because we might want to know the module info!
+// #ifdef GPS_UBLOX (unless it's a ublox, because we might want to know the module info!
// return GNSS_MODEL_UBLOX; think about removing this macro and return)
#if defined(GPS_L76K)
return GNSS_MODEL_MTK;
@@ -891,7 +892,8 @@ GnssModel_t GPS::probe()
}
}
- uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x0E, 0x30};
+ uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
+ UBXChecksum(cfg_rate, sizeof(cfg_rate));
_serial_gps->write(cfg_rate, sizeof(cfg_rate));
// Check that the returned response class and message ID are correct
if (!getAck(buffer, 384, 0x06, 0x08)) {
diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp
index d4ed6fdb6..9ccd28aba 100644
--- a/src/graphics/Screen.cpp
+++ b/src/graphics/Screen.cpp
@@ -36,11 +36,11 @@ along with this program. If not, see .
#include "mesh-pb-constants.h"
#include "mesh/Channels.h"
#include "mesh/generated/meshtastic/deviceonly.pb.h"
+#include "meshUtils.h"
#include "modules/ExternalNotificationModule.h"
#include "modules/TextMessageModule.h"
#include "sleep.h"
#include "target_specific.h"
-#include "utils.h"
#ifdef ARCH_ESP32
#include "esp_task_wdt.h"
@@ -365,7 +365,7 @@ static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *sta
// Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet)
{
- return packet->from != 0 && !moduleConfig.range_test.enabled && !moduleConfig.store_forward.enabled;
+ return packet->from != 0 && !moduleConfig.store_forward.enabled;
}
/// Draw the last text message we received
diff --git a/src/input/BBQ10Keyboard.cpp b/src/input/BBQ10Keyboard.cpp
new file mode 100644
index 000000000..8f8399aef
--- /dev/null
+++ b/src/input/BBQ10Keyboard.cpp
@@ -0,0 +1,181 @@
+// Based on arturo182 arduino_bbq10kbd library https://github.com/arturo182/arduino_bbq10kbd
+
+#include
+
+#include "BBQ10Keyboard.h"
+
+#define _REG_VER 1
+#define _REG_CFG 2
+#define _REG_INT 3
+#define _REG_KEY 4
+#define _REG_BKL 5
+#define _REG_DEB 6
+#define _REG_FRQ 7
+#define _REG_RST 8
+#define _REG_FIF 9
+
+#define _WRITE_MASK (1 << 7)
+
+#define CFG_OVERFLOW_ON (1 << 0)
+#define CFG_OVERFLOW_INT (1 << 1)
+#define CFG_CAPSLOCK_INT (1 << 2)
+#define CFG_NUMLOCK_INT (1 << 3)
+#define CFG_KEY_INT (1 << 4)
+#define CFG_PANIC_INT (1 << 5)
+#define CFG_REPORT_MODS (1 << 6)
+#define CFG_USE_MODS (1 << 7)
+
+#define INT_OVERFLOW (1 << 0)
+#define INT_CAPSLOCK (1 << 1)
+#define INT_NUMLOCK (1 << 2)
+#define INT_KEY (1 << 3)
+#define INT_PANIC (1 << 4)
+
+#define KEY_CAPSLOCK (1 << 5)
+#define KEY_NUMLOCK (1 << 6)
+#define KEY_COUNT_MASK (0x1F)
+
+BBQ10Keyboard::BBQ10Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr) {}
+
+void BBQ10Keyboard::begin(uint8_t addr, TwoWire *wire)
+{
+ m_addr = addr;
+ m_wire = wire;
+
+ m_wire->begin();
+
+ reset();
+}
+
+void BBQ10Keyboard::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr)
+{
+ m_addr = addr;
+ m_wire = nullptr;
+ writeCallback = w;
+ readCallback = r;
+ reset();
+}
+
+void BBQ10Keyboard::reset()
+{
+ if (m_wire) {
+ m_wire->beginTransmission(m_addr);
+ m_wire->write(_REG_RST);
+ m_wire->endTransmission();
+ }
+ if (writeCallback) {
+ uint8_t data = 0;
+ writeCallback(m_addr, _REG_RST, &data, 0);
+ }
+ delay(100);
+ writeRegister(_REG_CFG, readRegister8(_REG_CFG) | CFG_REPORT_MODS);
+ delay(100);
+}
+
+void BBQ10Keyboard::attachInterrupt(uint8_t pin, void (*func)(void)) const
+{
+ pinMode(pin, INPUT_PULLUP);
+ ::attachInterrupt(digitalPinToInterrupt(pin), func, RISING);
+}
+
+void BBQ10Keyboard::detachInterrupt(uint8_t pin) const
+{
+ ::detachInterrupt(pin);
+}
+
+void BBQ10Keyboard::clearInterruptStatus()
+{
+ writeRegister(_REG_INT, 0x00);
+}
+
+uint8_t BBQ10Keyboard::status() const
+{
+ return readRegister8(_REG_KEY);
+}
+
+uint8_t BBQ10Keyboard::keyCount() const
+{
+ return status() & KEY_COUNT_MASK;
+}
+
+BBQ10Keyboard::KeyEvent BBQ10Keyboard::keyEvent() const
+{
+ KeyEvent event = {.key = '\0', .state = StateIdle};
+
+ if (keyCount() == 0)
+ return event;
+
+ const uint16_t buf = readRegister16(_REG_FIF);
+ event.key = buf >> 8;
+ event.state = KeyState(buf & 0xFF);
+
+ return event;
+}
+
+float BBQ10Keyboard::backlight() const
+{
+ return readRegister8(_REG_BKL) / 255.0f;
+}
+
+void BBQ10Keyboard::setBacklight(float value)
+{
+ writeRegister(_REG_BKL, value * 255);
+}
+
+uint8_t BBQ10Keyboard::readRegister8(uint8_t reg) const
+{
+ if (m_wire) {
+ m_wire->beginTransmission(m_addr);
+ m_wire->write(reg);
+ m_wire->endTransmission();
+
+ m_wire->requestFrom(m_addr, (uint8_t)1);
+ if (m_wire->available() < 1)
+ return 0;
+
+ return m_wire->read();
+ }
+ if (readCallback) {
+ uint8_t data;
+ readCallback(m_addr, reg, &data, 1);
+ return data;
+ }
+ return 0;
+}
+
+uint16_t BBQ10Keyboard::readRegister16(uint8_t reg) const
+{
+ uint8_t data[2] = {0};
+ // uint8_t low = 0, high = 0;
+ if (m_wire) {
+ m_wire->beginTransmission(m_addr);
+ m_wire->write(reg);
+ m_wire->endTransmission();
+
+ m_wire->requestFrom(m_addr, (uint8_t)2);
+ if (m_wire->available() < 2)
+ return 0;
+ data[0] = m_wire->read();
+ data[1] = m_wire->read();
+ }
+ if (readCallback) {
+ readCallback(m_addr, reg, data, 2);
+ }
+ return (data[1] << 8) | data[0];
+}
+
+void BBQ10Keyboard::writeRegister(uint8_t reg, uint8_t value)
+{
+ uint8_t data[2];
+ data[0] = reg | _WRITE_MASK;
+ data[1] = value;
+
+ if (m_wire) {
+ m_wire->beginTransmission(m_addr);
+ m_wire->write(data, sizeof(uint8_t) * 2);
+ m_wire->endTransmission();
+ }
+ if (writeCallback) {
+ writeCallback(m_addr, data[0], &(data[1]), 1);
+ }
+}
diff --git a/src/input/BBQ10Keyboard.h b/src/input/BBQ10Keyboard.h
new file mode 100644
index 000000000..07d02308f
--- /dev/null
+++ b/src/input/BBQ10Keyboard.h
@@ -0,0 +1,51 @@
+// Based on arturo182 arduino_bbq10kbd library https://github.com/arturo182/arduino_bbq10kbd
+
+#include "configuration.h"
+#include
+
+#define KEY_MOD_ALT (0x1A)
+#define KEY_MOD_SHL (0x1B)
+#define KEY_MOD_SHR (0x1C)
+#define KEY_MOD_SYM (0x1D)
+
+class BBQ10Keyboard
+{
+ public:
+ typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len);
+
+ enum KeyState { StateIdle = 0, StatePress, StateLongPress, StateRelease };
+
+ struct KeyEvent {
+ char key;
+ KeyState state;
+ };
+
+ BBQ10Keyboard();
+
+ void begin(uint8_t addr = BBQ10_KB_ADDR, TwoWire *wire = &Wire);
+
+ void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = BBQ10_KB_ADDR);
+
+ void reset(void);
+
+ void attachInterrupt(uint8_t pin, void (*func)(void)) const;
+ void detachInterrupt(uint8_t pin) const;
+ void clearInterruptStatus(void);
+
+ uint8_t status(void) const;
+ uint8_t keyCount(void) const;
+ KeyEvent keyEvent(void) const;
+
+ float backlight() const;
+ void setBacklight(float value);
+
+ uint8_t readRegister8(uint8_t reg) const;
+ uint16_t readRegister16(uint8_t reg) const;
+ void writeRegister(uint8_t reg, uint8_t value);
+
+ private:
+ TwoWire *m_wire;
+ uint8_t m_addr;
+ i2c_com_fptr_t readCallback;
+ i2c_com_fptr_t writeCallback;
+};
diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp
index 44db1d952..e000f36eb 100644
--- a/src/input/cardKbI2cImpl.cpp
+++ b/src/input/cardKbI2cImpl.cpp
@@ -7,7 +7,7 @@ CardKbI2cImpl::CardKbI2cImpl() : KbI2cBase("cardKB") {}
void CardKbI2cImpl::init()
{
- if (cardkb_found.address != CARDKB_ADDR && cardkb_found.address != TDECK_KB_ADDR) {
+ if (cardkb_found.address == 0x00) {
disable();
return;
}
diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp
index f7fd62645..366e7fbb1 100644
--- a/src/input/kbI2cBase.cpp
+++ b/src/input/kbI2cBase.cpp
@@ -30,7 +30,7 @@ uint8_t read_from_14004(TwoWire *i2cBus, uint8_t reg, uint8_t *data, uint8_t len
int32_t KbI2cBase::runOnce()
{
- if (cardkb_found.address != CARDKB_ADDR && cardkb_found.address != TDECK_KB_ADDR) {
+ if (cardkb_found.address == 0x00) {
// Input device is not detected.
return INT32_MAX;
}
@@ -41,11 +41,19 @@ int32_t KbI2cBase::runOnce()
#ifdef I2C_SDA1
LOG_DEBUG("Using I2C Bus 1 (the second one)\n");
i2cBus = &Wire1;
+ if (cardkb_found.address == BBQ10_KB_ADDR) {
+ Q10keyboard.begin(BBQ10_KB_ADDR, &Wire1);
+ Q10keyboard.setBacklight(0);
+ }
break;
#endif
case ScanI2C::WIRE:
LOG_DEBUG("Using I2C Bus 0 (the first one)\n");
i2cBus = &Wire;
+ if (cardkb_found.address == BBQ10_KB_ADDR) {
+ Q10keyboard.begin(BBQ10_KB_ADDR, &Wire);
+ Q10keyboard.setBacklight(0);
+ }
break;
case ScanI2C::NO_I2C:
default:
@@ -53,7 +61,105 @@ int32_t KbI2cBase::runOnce()
}
}
- if (kb_model == 0x02) {
+ switch (kb_model) {
+ case 0x11: { // BB Q10
+ int keyCount = Q10keyboard.keyCount();
+ while (keyCount--) {
+ const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent();
+ if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) {
+ InputEvent e;
+ e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE;
+ e.source = this->_originName;
+ switch (key.key) {
+ case 'p': // TAB
+ case 't': // TAB as well
+ if (is_sym) {
+ e.inputEvent = ANYKEY;
+ e.kbchar = 0x09; // TAB Scancode
+ is_sym = false; // reset sym state after second keypress
+ } else {
+ e.inputEvent = ANYKEY;
+ e.kbchar = key.key;
+ }
+ break;
+ case 'q': // ESC
+ if (is_sym) {
+ e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL;
+ e.kbchar = 0x1b;
+ is_sym = false; // reset sym state after second keypress
+ } else {
+ e.inputEvent = ANYKEY;
+ e.kbchar = key.key;
+ }
+ break;
+ case 0x08: // Back
+ e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK;
+ e.kbchar = key.key;
+ break;
+ case 'e': // sym e
+ if (is_sym) {
+ e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP;
+ e.kbchar = 0xb5;
+ is_sym = false; // reset sym state after second keypress
+ } else {
+ e.inputEvent = ANYKEY;
+ e.kbchar = key.key;
+ }
+ break;
+ case 'x': // sym x
+ if (is_sym) {
+ e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN;
+ e.kbchar = 0xb6;
+ is_sym = false; // reset sym state after second keypress
+ } else {
+ e.inputEvent = ANYKEY;
+ e.kbchar = key.key;
+ }
+ break;
+ case 's': // sym s
+ if (is_sym) {
+ e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT;
+ e.kbchar = 0x00; // tweak for destSelect
+ is_sym = false; // reset sym state after second keypress
+ } else {
+ e.inputEvent = ANYKEY;
+ e.kbchar = key.key;
+ }
+ break;
+ case 'f': // sym f
+ if (is_sym) {
+ e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT;
+ e.kbchar = 0x00; // tweak for destSelect
+ is_sym = false; // reset sym state after second keypress
+ } else {
+ e.inputEvent = ANYKEY;
+ e.kbchar = key.key;
+ }
+ break;
+ case 0x13: // Code scanner says the SYM key is 0x13
+ is_sym = !is_sym;
+ break;
+ case 0x0a: // apparently Enter on Q10 is a line feed instead of carriage return
+ e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT;
+ break;
+ case 0x00: // nopress
+ e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE;
+ break;
+ default: // all other keys
+ e.inputEvent = ANYKEY;
+ e.kbchar = key.key;
+ is_sym = false; // reset sym state after second keypress
+ break;
+ }
+
+ if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) {
+ this->notifyObservers(&e);
+ }
+ }
+ }
+ break;
+ }
+ case 0x02: {
// RAK14004
uint8_t rDataBuf[8] = {0};
uint8_t PrintDataBuf = 0;
@@ -74,9 +180,12 @@ int32_t KbI2cBase::runOnce()
e.kbchar = PrintDataBuf;
this->notifyObservers(&e);
}
- } else if (kb_model == 0x00 || kb_model == 0x10) {
- // m5 cardkb and T-Deck
- i2cBus->requestFrom(kb_model == 0x00 ? CARDKB_ADDR : TDECK_KB_ADDR, 1);
+ break;
+ }
+ case 0x00: // CARDKB
+ case 0x10: { // T-DECK
+
+ i2cBus->requestFrom((int)cardkb_found.address, 1);
while (i2cBus->available()) {
char c = i2cBus->read();
@@ -93,17 +202,19 @@ int32_t KbI2cBase::runOnce()
break;
case 0xb5: // Up
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP;
+ e.kbchar = 0xb5;
break;
case 0xb6: // Down
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN;
+ e.kbchar = 0xb6;
break;
case 0xb4: // Left
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT;
- e.kbchar = c;
+ e.kbchar = 0xb4;
break;
case 0xb7: // Right
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT;
- e.kbchar = c;
+ e.kbchar = 0xb7;
break;
case 0x0d: // Enter
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT;
@@ -121,7 +232,9 @@ int32_t KbI2cBase::runOnce()
this->notifyObservers(&e);
}
}
- } else {
+ break;
+ }
+ default:
LOG_WARN("Unknown kb_model 0x%02x\n", kb_model);
}
return 300;
diff --git a/src/input/kbI2cBase.h b/src/input/kbI2cBase.h
index a0a4dd608..35b9b0901 100644
--- a/src/input/kbI2cBase.h
+++ b/src/input/kbI2cBase.h
@@ -1,5 +1,6 @@
#pragma once
+#include "BBQ10Keyboard.h"
#include "InputBroker.h"
#include "Wire.h"
#include "concurrency/OSThread.h"
@@ -16,4 +17,7 @@ class KbI2cBase : public Observable, public concurrency::OST
const char *_originName;
TwoWire *i2cBus = 0;
+
+ BBQ10Keyboard Q10keyboard;
+ bool is_sym = false;
};
diff --git a/src/main.cpp b/src/main.cpp
index d8311d555..987ed8962 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -387,6 +387,10 @@ void setup()
// assign an arbitrary value to distinguish from other models
kb_model = 0x10;
break;
+ case ScanI2C::DeviceType::BBQ10KB:
+ // assign an arbitrary value to distinguish from other models
+ kb_model = 0x11;
+ break;
default:
// use this as default since it's also just zero
LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00\n", kb_info.type);
diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp
index 86a577b1b..3f93649bb 100644
--- a/src/mesh/MeshService.cpp
+++ b/src/mesh/MeshService.cpp
@@ -327,15 +327,15 @@ int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus)
// load data from GPS object, will add timestamp + battery further down
pos = gps->p;
} else {
- // The GPS has lost lock, if we are fixed position we should just keep using
- // the old position
+ // The GPS has lost lock
#ifdef GPS_EXTRAVERBOSE
LOG_DEBUG("onGPSchanged() - lost validLocation\n");
#endif
- if (config.position.fixed_position) {
- LOG_WARN("Using fixed position\n");
- pos = ConvertToPosition(node->position);
- }
+ }
+ // Used fixed position if configured regalrdless of GPS lock
+ if (config.position.fixed_position) {
+ LOG_WARN("Using fixed position\n");
+ pos = ConvertToPosition(node->position);
}
// Finally add a fresh timestamp and battery level reading
diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h
index fa184b391..eb40b7712 100644
--- a/src/mesh/MeshService.h
+++ b/src/mesh/MeshService.h
@@ -48,6 +48,14 @@ class MeshService
uint32_t oldFromNum = 0;
public:
+ static bool isTextPayload(const meshtastic_MeshPacket *p)
+ {
+ if (moduleConfig.range_test.enabled && p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) {
+ return true;
+ }
+ return p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP ||
+ p->decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP;
+ }
/// Called when some new packets have arrived from one of the radios
Observable fromNumChanged;
diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h
index f319c58b9..887e4d11e 100644
--- a/src/mesh/generated/meshtastic/mesh.pb.h
+++ b/src/mesh/generated/meshtastic/mesh.pb.h
@@ -111,6 +111,8 @@ typedef enum _meshtastic_HardwareModel {
meshtastic_HardwareModel_T_WATCH_S3 = 51,
/* Bobricius Picomputer with ESP32-S3 CPU, Keyboard and IPS display */
meshtastic_HardwareModel_PICOMPUTER_S3 = 52,
+ /* Heltec HT-CT62 with ESP32-C3 CPU and SX1262 LoRa */
+ meshtastic_HardwareModel_HELTEC_HT62 = 53,
/* ------------------------------------------------------------------------------------------------------------------------------------------
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
------------------------------------------------------------------------------------------------------------------------------------------ */
diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h
index 44e9df70d..d354b7a42 100644
--- a/src/mesh/generated/meshtastic/portnums.pb.h
+++ b/src/mesh/generated/meshtastic/portnums.pb.h
@@ -69,6 +69,8 @@ typedef enum _meshtastic_PortNum {
NOTE: audio frames contain a 3 byte header (0xc0 0xde 0xc2) and a one byte marker for the decompressed bitrate.
This marker comes from the 'moduleConfig.audio.bitrate' enum minus one. */
meshtastic_PortNum_AUDIO_APP = 9,
+ /* Same as Text Message but originating from Detection Sensor Module. */
+ meshtastic_PortNum_DETECTION_SENSOR_APP = 10,
/* Provides a 'ping' service that replies to any packet it receives.
Also serves as a small example module.
ENCODING: ASCII Plaintext */
diff --git a/src/utils.h b/src/meshUtils.h
similarity index 100%
rename from src/utils.h
rename to src/meshUtils.h
diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp
index 1e605656f..ade9d0e5a 100644
--- a/src/modules/CannedMessageModule.cpp
+++ b/src/modules/CannedMessageModule.cpp
@@ -65,8 +65,8 @@ CannedMessageModule::CannedMessageModule()
{
if (moduleConfig.canned_message.enabled || CANNED_MESSAGE_MODULE_ENABLE) {
this->loadProtoForModule();
- if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address != CARDKB_ADDR) &&
- (cardkb_found.address != TDECK_KB_ADDR) && !INPUTBROKER_MATRIX_TYPE && !CANNED_MESSAGE_MODULE_ENABLE) {
+ if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE &&
+ !CANNED_MESSAGE_MODULE_ENABLE) {
LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled\n");
this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED;
disable();
@@ -171,7 +171,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
(event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) ||
(event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT))) {
// LOG_DEBUG("Canned message event (%x)\n", event->kbchar);
- // tweak for left/right events generated via trackball/touch with empty kbchar
+ // tweak for left/right events generated via trackball/touch with empty kbchar
if (!event->kbchar) {
if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) {
this->payload = 0xb4;
@@ -195,6 +195,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
}
// pass the pressed key
+ // LOG_DEBUG("Canned message ANYKEY (%x)\n", event->kbchar);
this->payload = event->kbchar;
this->lastTouchMillis = millis();
validEvent = true;
@@ -649,4 +650,4 @@ String CannedMessageModule::drawWithCursor(String text, int cursor)
return result;
}
-#endif
\ No newline at end of file
+#endif
diff --git a/src/modules/DetectionSensorModule.cpp b/src/modules/DetectionSensorModule.cpp
index c8eb7793b..f1fc26244 100644
--- a/src/modules/DetectionSensorModule.cpp
+++ b/src/modules/DetectionSensorModule.cpp
@@ -46,10 +46,7 @@ int32_t DetectionSensorModule::runOnce()
if ((millis() - lastSentToMesh) >= getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs) &&
hasDetectionEvent()) {
sendDetectionMessage();
- return getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs <
- moduleConfig.detection_sensor.state_broadcast_secs
- ? moduleConfig.detection_sensor.minimum_broadcast_secs
- : moduleConfig.detection_sensor.state_broadcast_secs);
+ return DELAYED_INTERVAL;
}
// Even if we haven't detected an event, broadcast our current state to the mesh on the scheduled interval as a sort
// of heartbeat. We only do this if the minimum broadcast interval is greater than zero, otherwise we'll only broadcast state
@@ -57,10 +54,7 @@ int32_t DetectionSensorModule::runOnce()
else if (moduleConfig.detection_sensor.state_broadcast_secs > 0 &&
(millis() - lastSentToMesh) >= getConfiguredOrDefaultMs(moduleConfig.detection_sensor.state_broadcast_secs)) {
sendCurrentStateMessage();
- return getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs <
- moduleConfig.detection_sensor.state_broadcast_secs
- ? moduleConfig.detection_sensor.minimum_broadcast_secs
- : moduleConfig.detection_sensor.state_broadcast_secs);
+ return DELAYED_INTERVAL;
}
return GPIO_POLLING_INTERVAL;
}
diff --git a/src/modules/DetectionSensorModule.h b/src/modules/DetectionSensorModule.h
index bcc0b9419..ed6cddda5 100644
--- a/src/modules/DetectionSensorModule.h
+++ b/src/modules/DetectionSensorModule.h
@@ -5,7 +5,7 @@ class DetectionSensorModule : public SinglePortModule, private concurrency::OSTh
{
public:
DetectionSensorModule()
- : SinglePortModule("detection", meshtastic_PortNum_TEXT_MESSAGE_APP), OSThread("DetectionSensorModule")
+ : SinglePortModule("detection", meshtastic_PortNum_DETECTION_SENSOR_APP), OSThread("DetectionSensorModule")
{
}
diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp
index cbcf4e452..20191e706 100644
--- a/src/modules/ExternalNotificationModule.cpp
+++ b/src/modules/ExternalNotificationModule.cpp
@@ -128,6 +128,11 @@ int32_t ExternalNotificationModule::runOnce()
}
}
+bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p)
+{
+ return MeshService::isTextPayload(p);
+}
+
/**
* Sets the external notification on for the specified index.
*
@@ -212,8 +217,8 @@ void ExternalNotificationModule::stopNow()
}
ExternalNotificationModule::ExternalNotificationModule()
- : SinglePortModule("ExternalNotificationModule", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread(
- "ExternalNotificationModule")
+ : SinglePortModule("ExternalNotificationModule", meshtastic_PortNum_TEXT_MESSAGE_APP),
+ concurrency::OSThread("ExternalNotificationModule")
{
/*
Uncomment the preferences below if you want to use the module
diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h
index f8ec053dd..3331ec428 100644
--- a/src/modules/ExternalNotificationModule.h
+++ b/src/modules/ExternalNotificationModule.h
@@ -52,6 +52,8 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency:
virtual int32_t runOnce() override;
+ virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
+
bool isNagging = false;
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
@@ -59,4 +61,4 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency:
meshtastic_AdminMessage *response) override;
};
-extern ExternalNotificationModule *externalNotificationModule;
+extern ExternalNotificationModule *externalNotificationModule;
\ No newline at end of file
diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp
index 706b1db58..a5bb671f0 100644
--- a/src/modules/NeighborInfoModule.cpp
+++ b/src/modules/NeighborInfoModule.cpp
@@ -15,12 +15,12 @@ NOTE: For debugging only
*/
void NeighborInfoModule::printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np)
{
- LOG_DEBUG("%s NEIGHBORINFO PACKET from Node %d to Node %d (last sent by %d)\n", header, np->node_id, nodeDB.getNodeNum(),
- np->last_sent_by_id);
+ LOG_DEBUG("%s NEIGHBORINFO PACKET from Node 0x%x to Node 0x%x (last sent by 0x%x)\n", header, np->node_id,
+ nodeDB.getNodeNum(), np->last_sent_by_id);
LOG_DEBUG("----------------\n");
LOG_DEBUG("Packet contains %d neighbors\n", np->neighbors_count);
for (int i = 0; i < np->neighbors_count; i++) {
- LOG_DEBUG("Neighbor %d: node_id=%d, snr=%.2f\n", i, np->neighbors[i].node_id, np->neighbors[i].snr);
+ LOG_DEBUG("Neighbor %d: node_id=0x%x, snr=%.2f\n", i, np->neighbors[i].node_id, np->neighbors[i].snr);
}
LOG_DEBUG("----------------\n");
}
@@ -31,12 +31,12 @@ NOTE: for debugging only
void NeighborInfoModule::printNodeDBNodes(const char *header)
{
int num_nodes = nodeDB.getNumMeshNodes();
- LOG_DEBUG("%s NODEDB SELECTION from Node %d:\n", header, nodeDB.getNodeNum());
+ LOG_DEBUG("%s NODEDB SELECTION from Node 0x%x:\n", header, nodeDB.getNodeNum());
LOG_DEBUG("----------------\n");
LOG_DEBUG("DB contains %d nodes\n", num_nodes);
for (int i = 0; i < num_nodes; i++) {
const meshtastic_NodeInfoLite *dbEntry = nodeDB.getMeshNodeByIndex(i);
- LOG_DEBUG(" Node %d: node_id=%d, snr=%.2f\n", i, dbEntry->num, dbEntry->snr);
+ LOG_DEBUG(" Node %d: node_id=0x%x, snr=%.2f\n", i, dbEntry->num, dbEntry->snr);
}
LOG_DEBUG("----------------\n");
}
@@ -48,12 +48,12 @@ NOTE: for debugging only
void NeighborInfoModule::printNodeDBNeighbors(const char *header)
{
int num_neighbors = getNumNeighbors();
- LOG_DEBUG("%s NODEDB SELECTION from Node %d:\n", header, nodeDB.getNodeNum());
+ LOG_DEBUG("%s NODEDB SELECTION from Node 0x%x:\n", header, nodeDB.getNodeNum());
LOG_DEBUG("----------------\n");
LOG_DEBUG("DB contains %d neighbors\n", num_neighbors);
for (int i = 0; i < num_neighbors; i++) {
const meshtastic_Neighbor *dbEntry = getNeighborByIndex(i);
- LOG_DEBUG(" Node %d: node_id=%d, snr=%.2f\n", i, dbEntry->node_id, dbEntry->snr);
+ LOG_DEBUG(" Node %d: node_id=0x%x, snr=%.2f\n", i, dbEntry->node_id, dbEntry->snr);
}
LOG_DEBUG("----------------\n");
}
@@ -66,7 +66,7 @@ NOTE: For debugging only
void NeighborInfoModule::printNodeDBSelection(const char *header, const meshtastic_NeighborInfo *np)
{
int num_neighbors = getNumNeighbors();
- LOG_DEBUG("%s NODEDB SELECTION from Node %d:\n", header, nodeDB.getNodeNum());
+ LOG_DEBUG("%s NODEDB SELECTION from Node 0x%x:\n", header, nodeDB.getNodeNum());
LOG_DEBUG("----------------\n");
LOG_DEBUG("Selected %d neighbors of %d DB neighbors\n", np->neighbors_count, num_neighbors);
for (int i = 0; i < num_neighbors; i++) {
@@ -78,9 +78,9 @@ void NeighborInfoModule::printNodeDBSelection(const char *header, const meshtast
}
}
if (!chosen) {
- LOG_DEBUG(" Node %d: neighbor=%d, snr=%.2f\n", i, dbEntry->node_id, dbEntry->snr);
+ LOG_DEBUG(" Node %d: neighbor=0x%x, snr=%.2f\n", i, dbEntry->node_id, dbEntry->snr);
} else {
- LOG_DEBUG("---> Node %d: neighbor=%d, snr=%.2f\n", i, dbEntry->node_id, dbEntry->snr);
+ LOG_DEBUG("---> Node %d: neighbor=0x%x, snr=%.2f\n", i, dbEntry->node_id, dbEntry->snr);
}
}
LOG_DEBUG("----------------\n");
@@ -120,7 +120,7 @@ Assumes that the neighborInfo packet has been allocated
*/
uint32_t NeighborInfoModule::collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo)
{
- int my_node_id = nodeDB.getNodeNum();
+ uint my_node_id = nodeDB.getNodeNum();
neighborInfo->node_id = my_node_id;
neighborInfo->last_sent_by_id = my_node_id;
neighborInfo->node_broadcast_interval_secs = moduleConfig.neighbor_info.update_interval;
@@ -255,7 +255,7 @@ void NeighborInfoModule::updateNeighbors(const meshtastic_MeshPacket &mp, const
}
meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSender, NodeNum n,
- uint32_t node_broadcast_interval_secs, int snr)
+ uint32_t node_broadcast_interval_secs, float snr)
{
// our node and the phone are the same node (not neighbors)
if (n == 0) {
diff --git a/src/modules/NeighborInfoModule.h b/src/modules/NeighborInfoModule.h
index 20f68ca87..0e3ec09ca 100644
--- a/src/modules/NeighborInfoModule.h
+++ b/src/modules/NeighborInfoModule.h
@@ -49,7 +49,7 @@ class NeighborInfoModule : public ProtobufModule, priva
meshtastic_NeighborInfo *allocateNeighborInfoPacket();
// Find a neighbor in our DB, create an empty neighbor if missing
- meshtastic_Neighbor *getOrCreateNeighbor(NodeNum originalSender, NodeNum n, uint32_t node_broadcast_interval_secs, int snr);
+ meshtastic_Neighbor *getOrCreateNeighbor(NodeNum originalSender, NodeNum n, uint32_t node_broadcast_interval_secs, float snr);
/*
* Send info on our node's neighbors into the mesh
diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp
index cb49f0a8f..40dbf266e 100644
--- a/src/modules/PositionModule.cpp
+++ b/src/modules/PositionModule.cpp
@@ -11,8 +11,8 @@
PositionModule *positionModule;
PositionModule::PositionModule()
- : ProtobufModule("position", meshtastic_PortNum_POSITION_APP, &meshtastic_Position_msg), concurrency::OSThread(
- "PositionModule")
+ : ProtobufModule("position", meshtastic_PortNum_POSITION_APP, &meshtastic_Position_msg),
+ concurrency::OSThread("PositionModule")
{
isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others
setIntervalFromNow(60 * 1000); // Send our initial position 60 seconds after we start (to give GPS time to setup)
@@ -65,7 +65,7 @@ bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
meshtastic_MeshPacket *PositionModule::allocReply()
{
if (ignoreRequest) {
- return NULL;
+ return nullptr;
}
meshtastic_NodeInfoLite *node = service.refreshLocalMeshNode(); // should guarantee there is now a position
@@ -142,6 +142,11 @@ void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha
service.cancelSending(prevPacketId);
meshtastic_MeshPacket *p = allocReply();
+ if (p == nullptr) {
+ LOG_WARN("allocReply returned a nullptr");
+ return;
+ }
+
p->to = dest;
p->decoded.want_response = wantReplies;
if (config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER)
diff --git a/src/modules/RangeTestModule.h b/src/modules/RangeTestModule.h
index ae2a8f182..b632d343e 100644
--- a/src/modules/RangeTestModule.h
+++ b/src/modules/RangeTestModule.h
@@ -29,7 +29,7 @@ class RangeTestModuleRadio : public SinglePortModule
uint32_t lastRxID = 0;
public:
- RangeTestModuleRadio() : SinglePortModule("RangeTestModuleRadio", meshtastic_PortNum_TEXT_MESSAGE_APP)
+ RangeTestModuleRadio() : SinglePortModule("RangeTestModuleRadio", meshtastic_PortNum_RANGE_TEST_APP)
{
loopbackOk = true; // Allow locally generated messages to loop back to the client
}
diff --git a/src/modules/TextMessageModule.cpp b/src/modules/TextMessageModule.cpp
index 8ff034fa9..0f86a6470 100644
--- a/src/modules/TextMessageModule.cpp
+++ b/src/modules/TextMessageModule.cpp
@@ -1,4 +1,5 @@
#include "TextMessageModule.h"
+#include "MeshService.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "configuration.h"
@@ -22,3 +23,8 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}
+
+bool TextMessageModule::wantPacket(const meshtastic_MeshPacket *p)
+{
+ return MeshService::isTextPayload(p);
+}
\ No newline at end of file
diff --git a/src/modules/TextMessageModule.h b/src/modules/TextMessageModule.h
index 93b1bfaa2..cc0b0f9d5 100644
--- a/src/modules/TextMessageModule.h
+++ b/src/modules/TextMessageModule.h
@@ -20,6 +20,7 @@ class TextMessageModule : public SinglePortModule, public Observable
#include
#include
@@ -220,4 +220,4 @@ void cpuDeepSleep(uint32_t msecToWake)
esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs
esp_deep_sleep_start(); // TBD mA sleep current (battery)
-}
+}
\ No newline at end of file
diff --git a/variants/heltec_esp32c3/pins_arduino.h b/variants/heltec_esp32c3/pins_arduino.h
new file mode 100644
index 000000000..db30a2f30
--- /dev/null
+++ b/variants/heltec_esp32c3/pins_arduino.h
@@ -0,0 +1,32 @@
+#ifndef Pins_Arduino_h
+#define Pins_Arduino_h
+
+#include
+
+#define EXTERNAL_NUM_INTERRUPTS 22
+#define NUM_DIGITAL_PINS 22
+#define NUM_ANALOG_INPUTS 6
+
+#define analogInputToDigitalPin(p) (((p) < NUM_ANALOG_INPUTS) ? (esp32_adc2gpio[(p)]) : -1)
+#define digitalPinToInterrupt(p) (((p) < NUM_DIGITAL_PINS) ? (p) : -1)
+#define digitalPinHasPWM(p) (p < EXTERNAL_NUM_INTERRUPTS)
+
+static const uint8_t TX = 21;
+static const uint8_t RX = 20;
+
+static const uint8_t SDA = 1;
+static const uint8_t SCL = 0;
+
+static const uint8_t SS = 8;
+static const uint8_t MOSI = 7;
+static const uint8_t MISO = 6;
+static const uint8_t SCK = 10;
+
+static const uint8_t A0 = 0;
+static const uint8_t A1 = 1;
+static const uint8_t A2 = 2;
+static const uint8_t A3 = 3;
+static const uint8_t A4 = 4;
+static const uint8_t A5 = 5;
+
+#endif /* Pins_Arduino_h */
\ No newline at end of file
diff --git a/variants/heltec_esp32c3/platformio.ini b/variants/heltec_esp32c3/platformio.ini
new file mode 100644
index 000000000..a9843ef85
--- /dev/null
+++ b/variants/heltec_esp32c3/platformio.ini
@@ -0,0 +1,12 @@
+[env:heltec-ht62-esp32c3-sx1262]
+extends = esp32c3_base
+board = esp32-c3-devkitm-1
+board_level = extra
+build_flags =
+ ${esp32_base.build_flags}
+ -D HELTEC_HT62
+ -I variants/heltec_esp32c3
+monitor_speed = 115200
+upload_protocol = esptool
+upload_port = /dev/ttyUSB0
+upload_speed = 921600
diff --git a/variants/heltec_esp32c3/variant.h b/variants/heltec_esp32c3/variant.h
new file mode 100644
index 000000000..29ed88080
--- /dev/null
+++ b/variants/heltec_esp32c3/variant.h
@@ -0,0 +1,36 @@
+#define I2C_SDA 1
+#define I2C_SCL 0
+
+#define BUTTON_PIN 9
+#define BUTTON_NEED_PULLUP
+
+// LED flashes brighter
+// https://resource.heltec.cn/download/HT-CT62/HT-CT62_Reference_Design.pdf
+#define LED_PIN 18 // LED
+#define LED_INVERTED 1
+
+#define HAS_SCREEN 0
+#define HAS_GPS 0
+#undef GPS_RX_PIN
+#undef GPS_TX_PIN
+
+#undef RF95_SCK
+#undef RF95_MISO
+#undef RF95_MOSI
+#undef RF95_NSS
+
+#define USE_SX1262
+#define RF95_SCK 10
+#define RF95_MISO 6
+#define RF95_MOSI 7
+#define RF95_NSS 8
+#define LORA_DIO0 RADIOLIB_NC
+#define LORA_RESET 5
+#define LORA_DIO1 3
+#define LORA_DIO2 RADIOLIB_NC
+#define LORA_BUSY 4
+#define SX126X_CS RF95_NSS
+#define SX126X_DIO1 LORA_DIO1
+#define SX126X_BUSY LORA_BUSY
+#define SX126X_RESET LORA_RESET
+#define SX126X_E22
diff --git a/version.properties b/version.properties
index f26bc2b58..830d7970e 100644
--- a/version.properties
+++ b/version.properties
@@ -1,4 +1,4 @@
[VERSION]
major = 2
minor = 2
-build = 2
+build = 4