mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-15 15:22:34 +00:00
Compare commits
40 Commits
indicator-
...
on-screen-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c95991cee2 | ||
|
|
f901166f16 | ||
|
|
ddc66c5e84 | ||
|
|
0544998c9b | ||
|
|
093a37a2b0 | ||
|
|
1daf5aad1f | ||
|
|
fe3f14a63e | ||
|
|
dca615fe3d | ||
|
|
cca2ead2c1 | ||
|
|
7574bfb7cb | ||
|
|
ce75bf4496 | ||
|
|
5ce47045e7 | ||
|
|
57e1725419 | ||
|
|
53afebae41 | ||
|
|
11309662a9 | ||
|
|
890357d579 | ||
|
|
4f4ede9c8c | ||
|
|
f413c49555 | ||
|
|
c19f573b49 | ||
|
|
5de61b1a3d | ||
|
|
1c1462e776 | ||
|
|
eb6ef1cbea | ||
|
|
9654f5b218 | ||
|
|
68726a1b0e | ||
|
|
5b62bbe8e6 | ||
|
|
e55084629a | ||
|
|
1691e885f2 | ||
|
|
2d7818797d | ||
|
|
f65e2c639e | ||
|
|
fbd4138d98 | ||
|
|
f4bb2ec0f0 | ||
|
|
95200e8f6b | ||
|
|
36e8dc74f4 | ||
|
|
995752e31d | ||
|
|
75b12d318d | ||
|
|
eea4d734d2 | ||
|
|
78c5309e9a | ||
|
|
e98c6debb2 | ||
|
|
ace45c1236 | ||
|
|
6c7da1e6b1 |
238
.github/workflows/pr_tests.yml
vendored
Normal file
238
.github/workflows/pr_tests.yml
vendored
Normal file
@@ -0,0 +1,238 @@
|
||||
name: Tests
|
||||
|
||||
# DISABLED: Changed from automatic PR triggers to manual only
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: "Reason for manual test run"
|
||||
required: false
|
||||
default: "Manual test execution"
|
||||
|
||||
concurrency:
|
||||
group: tests-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
native-tests:
|
||||
name: "🧪 Native Tests"
|
||||
if: github.repository == 'meshtastic/firmware'
|
||||
uses: ./.github/workflows/test_native.yml
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
|
||||
test-summary:
|
||||
name: "📊 Test Results"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [native-tests]
|
||||
if: always()
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Get release version string
|
||||
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||
id: version
|
||||
|
||||
- name: Download test artifacts
|
||||
if: needs.native-tests.result != 'skipped'
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
|
||||
merge-multiple: true
|
||||
|
||||
- name: Parse test results and create detailed summary
|
||||
id: test-results
|
||||
run: |
|
||||
echo "## 🧪 Test Results Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Check overall job status first
|
||||
if [[ "${{ needs.native-tests.result }}" == "success" ]]; then
|
||||
echo "✅ **Overall Status**: PASSED" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "${{ needs.native-tests.result }}" == "failure" ]]; then
|
||||
echo "❌ **Overall Status**: FAILED" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "${{ needs.native-tests.result }}" == "cancelled" ]]; then
|
||||
echo "⏸️ **Overall Status**: CANCELLED" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tests were cancelled before completion." >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
else
|
||||
echo "⚠️ **Overall Status**: SKIPPED" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tests were skipped." >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Parse detailed test results if available
|
||||
if [ -f "testreport.xml" ]; then
|
||||
echo "### 🔍 Individual Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
python3 << 'EOF'
|
||||
import xml.etree.ElementTree as ET
|
||||
import os
|
||||
|
||||
try:
|
||||
tree = ET.parse('testreport.xml')
|
||||
root = tree.getroot()
|
||||
|
||||
total_tests = 0
|
||||
passed_tests = 0
|
||||
failed_tests = 0
|
||||
skipped_tests = 0
|
||||
|
||||
# Parse testsuite elements
|
||||
for testsuite in root.findall('.//testsuite'):
|
||||
suite_name = testsuite.get('name', 'Unknown')
|
||||
suite_tests = int(testsuite.get('tests', '0'))
|
||||
suite_failures = int(testsuite.get('failures', '0'))
|
||||
suite_errors = int(testsuite.get('errors', '0'))
|
||||
suite_skipped = int(testsuite.get('skipped', '0'))
|
||||
|
||||
total_tests += suite_tests
|
||||
failed_tests += suite_failures + suite_errors
|
||||
skipped_tests += suite_skipped
|
||||
passed_tests += suite_tests - suite_failures - suite_errors - suite_skipped
|
||||
|
||||
if suite_tests > 0:
|
||||
status = "✅" if (suite_failures + suite_errors) == 0 else "❌"
|
||||
print(f"**{status} Test Suite: {suite_name}**")
|
||||
print(f"- Total: {suite_tests}")
|
||||
print(f"- Passed: ✅ {suite_tests - suite_failures - suite_errors - suite_skipped}")
|
||||
print(f"- Failed: ❌ {suite_failures + suite_errors}")
|
||||
if suite_skipped > 0:
|
||||
print(f"- Skipped: ⏭️ {suite_skipped}")
|
||||
print("")
|
||||
|
||||
# Show individual test results for failed suites
|
||||
if suite_failures + suite_errors > 0:
|
||||
print("**Failed Tests:**")
|
||||
for testcase in testsuite.findall('testcase'):
|
||||
test_name = testcase.get('name', 'Unknown')
|
||||
failure = testcase.find('failure')
|
||||
error = testcase.find('error')
|
||||
|
||||
if failure is not None:
|
||||
msg = failure.get('message', 'Unknown error')[:100]
|
||||
print(f"- ❌ `{test_name}`: {msg}")
|
||||
elif error is not None:
|
||||
msg = error.get('message', 'Unknown error')[:100]
|
||||
print(f"- ❌ `{test_name}`: ERROR - {msg}")
|
||||
print("")
|
||||
else:
|
||||
# Show passed tests for successful suites
|
||||
passed_count = 0
|
||||
for testcase in testsuite.findall('testcase'):
|
||||
if testcase.find('failure') is None and testcase.find('error') is None:
|
||||
if passed_count < 5: # Limit to first 5 to avoid spam
|
||||
test_name = testcase.get('name', 'Unknown')
|
||||
print(f"- ✅ `{test_name}`: PASSED")
|
||||
passed_count += 1
|
||||
if passed_count > 5:
|
||||
print(f"- ... and {passed_count - 5} more tests passed")
|
||||
print("")
|
||||
|
||||
# Summary statistics
|
||||
print("### 📊 Test Statistics")
|
||||
print(f"- **Total Tests**: {total_tests}")
|
||||
print(f"- **Passed**: ✅ {passed_tests}")
|
||||
print(f"- **Failed**: ❌ {failed_tests}")
|
||||
if skipped_tests > 0:
|
||||
print(f"- **Skipped**: ⏭️ {skipped_tests}")
|
||||
|
||||
if failed_tests > 0:
|
||||
print(f"\n❌ **{failed_tests} tests failed out of {total_tests} total**")
|
||||
else:
|
||||
print(f"\n✅ **All {total_tests} tests passed!**")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error parsing test results: {e}")
|
||||
EOF
|
||||
else
|
||||
echo "⚠️ **No detailed test report available**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Test artifacts may not have been generated properly." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "---" >> $GITHUB_STEP_SUMMARY
|
||||
echo "View detailed logs in the [Actions tab](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Comment test results on PR
|
||||
if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
// Read the step summary to use as PR comment
|
||||
let testSummary = "## 🧪 Test Results Summary\n\n";
|
||||
|
||||
if ("${{ needs.native-tests.result }}" === "success") {
|
||||
testSummary += "✅ **All tests passed!**\n\n";
|
||||
} else if ("${{ needs.native-tests.result }}" === "failure") {
|
||||
testSummary += "❌ **Some tests failed.**\n\n";
|
||||
} else {
|
||||
testSummary += "⚠️ **Tests did not complete normally.**\n\n";
|
||||
}
|
||||
|
||||
testSummary += `View detailed results: [Actions Run](${context.payload.repository.html_url}/actions/runs/${context.runId})\n\n`;
|
||||
testSummary += "---\n";
|
||||
testSummary += "*This comment will be automatically updated when new commits are pushed.*";
|
||||
|
||||
// Find existing comment
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
|
||||
const botComment = comments.data.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('🧪 Test Results Summary')
|
||||
);
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: testSummary
|
||||
});
|
||||
} else {
|
||||
// Create new comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: testSummary
|
||||
});
|
||||
}
|
||||
|
||||
- name: Set overall status
|
||||
run: |
|
||||
if [[ "${{ needs.native-tests.result }}" == "success" ]]; then
|
||||
echo "All tests passed! ✅"
|
||||
exit 0
|
||||
else
|
||||
echo "Some tests failed! ❌"
|
||||
exit 1
|
||||
fi
|
||||
@@ -9,9 +9,9 @@ plugins:
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.461
|
||||
- renovate@41.71.1
|
||||
- renovate@41.74.0
|
||||
- prettier@3.6.2
|
||||
- trufflehog@3.90.4
|
||||
- trufflehog@3.90.5
|
||||
- yamllint@1.37.1
|
||||
- bandit@1.8.6
|
||||
- trivy@0.64.1
|
||||
|
||||
@@ -61,7 +61,7 @@ RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||
|
||||
# Fetch compiled binary from the builder
|
||||
COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/
|
||||
COPY --from=builder /tmp/web /usr/share/meshtasticd/
|
||||
COPY --from=builder /tmp/web /usr/share/meshtasticd/web/
|
||||
# Copy config templates
|
||||
COPY ./bin/config.d /etc/meshtasticd/available.d
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[portduino_base]
|
||||
platform =
|
||||
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
|
||||
https://github.com/meshtastic/platform-native/archive/6cb7a455b440dd0738e8ed74a18136ed5cf7ea63.zip
|
||||
https://github.com/meshtastic/platform-native/archive/37d986499ce24511952d7146db72d667c6bdaff7.zip
|
||||
framework = arduino
|
||||
|
||||
build_src_filter =
|
||||
|
||||
@@ -9,13 +9,4 @@ Lora:
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
# CS: 8
|
||||
|
||||
|
||||
### RAK13300in Slot 2 pins
|
||||
# IRQ: 18 #IO6
|
||||
# Reset: 24 # IO4
|
||||
# Busy: 19 # IO5
|
||||
# # Ant_sw: 23 # IO3
|
||||
# spidev: spidev0.1
|
||||
# # CS: 7
|
||||
# CS: 8
|
||||
8
bin/config.d/lora-RAK6421-13300-slot2.yaml
Normal file
8
bin/config.d/lora-RAK6421-13300-slot2.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
Lora:
|
||||
### RAK13300in Slot 2 pins
|
||||
IRQ: 18 #IO6
|
||||
Reset: 24 # IO4
|
||||
Busy: 19 # IO5
|
||||
# Ant_sw: 23 # IO3
|
||||
spidev: spidev0.1
|
||||
# CS: 7
|
||||
52
boards/meshtiny.json
Normal file
52
boards/meshtiny.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x239A", "0x8029"],
|
||||
["0x239A", "0x0029"],
|
||||
["0x239A", "0x002A"],
|
||||
["0x239A", "0x802A"]
|
||||
],
|
||||
"usb_product": "MeshTiny",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "meshtiny",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino", "freertos"],
|
||||
"name": "MeshTiny",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "https://github.com/meshtastic/firmware",
|
||||
"vendor": "MTools Tec"
|
||||
}
|
||||
@@ -60,7 +60,7 @@ monitor_speed = 115200
|
||||
monitor_filters = direct
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0119501e9983bd894830b02f545c377ee08d66fe.zip
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||
@@ -118,7 +118,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/0cd108ff783539e41ef38258ba2784ab3b1bdc97.zip
|
||||
https://github.com/meshtastic/device-ui/archive/3dc7cf3e233aaa8cc23492cca50541fc099ebfa1.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
|
||||
Submodule protobufs updated: 5dd723fe6f...8985852d75
@@ -28,11 +28,14 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
|
||||
case INPUT_BROKER_USER_PRESS:
|
||||
case INPUT_BROKER_ALT_PRESS:
|
||||
case INPUT_BROKER_SELECT:
|
||||
case INPUT_BROKER_SELECT_LONG:
|
||||
playBeep(); // Confirmation feedback
|
||||
break;
|
||||
|
||||
case INPUT_BROKER_UP:
|
||||
case INPUT_BROKER_UP_LONG:
|
||||
case INPUT_BROKER_DOWN:
|
||||
case INPUT_BROKER_DOWN_LONG:
|
||||
case INPUT_BROKER_LEFT:
|
||||
case INPUT_BROKER_RIGHT:
|
||||
playChirp(); // Navigation feedback
|
||||
|
||||
@@ -216,6 +216,44 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
ui->update();
|
||||
}
|
||||
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback)
|
||||
{
|
||||
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
|
||||
|
||||
if (NotificationRenderer::virtualKeyboard) {
|
||||
delete NotificationRenderer::virtualKeyboard;
|
||||
NotificationRenderer::virtualKeyboard = nullptr;
|
||||
}
|
||||
|
||||
NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
NotificationRenderer::virtualKeyboard = new VirtualKeyboard();
|
||||
if (header) {
|
||||
NotificationRenderer::virtualKeyboard->setHeader(header);
|
||||
}
|
||||
if (initialText) {
|
||||
NotificationRenderer::virtualKeyboard->setInputText(initialText);
|
||||
}
|
||||
|
||||
// Set up callback with safer cleanup mechanism
|
||||
NotificationRenderer::textInputCallback = textCallback;
|
||||
NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); });
|
||||
|
||||
// Store the message and set the expiration timestamp (use same pattern as other notifications)
|
||||
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0';
|
||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::text_input;
|
||||
|
||||
// Set the overlay using the same pattern as other notification types
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
}
|
||||
|
||||
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
uint8_t module_frame;
|
||||
@@ -713,13 +751,19 @@ int32_t Screen::runOnce()
|
||||
handleSetOn(false);
|
||||
break;
|
||||
case Cmd::ON_PRESS:
|
||||
handleOnPress();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleOnPress();
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_PREV_FRAME:
|
||||
handleShowPrevFrame();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowPrevFrame();
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_NEXT_FRAME:
|
||||
handleShowNextFrame();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowNextFrame();
|
||||
}
|
||||
break;
|
||||
case Cmd::START_ALERT_FRAME: {
|
||||
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
|
||||
@@ -741,7 +785,9 @@ int32_t Screen::runOnce()
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
case Cmd::STOP_BOOT_SCREEN:
|
||||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
||||
setFrames();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
setFrames();
|
||||
}
|
||||
break;
|
||||
case Cmd::NOOP:
|
||||
break;
|
||||
@@ -777,6 +823,7 @@ int32_t Screen::runOnce()
|
||||
if (showingNormalScreen) {
|
||||
// standard screen loop handling here
|
||||
if (config.display.auto_screen_carousel_secs > 0 &&
|
||||
NotificationRenderer::current_notification_type != notificationTypeEnum::text_input &&
|
||||
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
|
||||
|
||||
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
|
||||
@@ -867,6 +914,11 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
||||
// Called when a frame should be added / removed, or custom frames should be cleared
|
||||
void Screen::setFrames(FrameFocus focus)
|
||||
{
|
||||
// Block setFrames calls when virtual keyboard is active to prevent overlay interference
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t originalPosition = ui->getUiState()->currentFrame;
|
||||
uint8_t previousFrameCount = framesetInfo.frameCount;
|
||||
FramesetInfo fsi; // Location of specific frames, for applying focus parameter
|
||||
@@ -1313,6 +1365,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
// Triggered by MeshModules
|
||||
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
||||
{
|
||||
// Block UI frame events when virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (showingNormalScreen) {
|
||||
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
||||
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
|
||||
@@ -1335,6 +1392,16 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (!screenOn)
|
||||
return 0;
|
||||
|
||||
// Handle text input notifications specially - pass input to virtual keyboard
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
NotificationRenderer::inEvent = *event;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
setFastFramerate(); // Draw ASAP
|
||||
ui->update();
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
|
||||
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
||||
namespace graphics
|
||||
{
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker };
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
|
||||
|
||||
struct BannerOverlayOptions {
|
||||
const char *message;
|
||||
@@ -313,6 +313,8 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback);
|
||||
|
||||
void requestMenu(graphics::menuHandler::screenMenus menuToShow)
|
||||
{
|
||||
|
||||
@@ -667,15 +667,19 @@ static LGFX *tft = nullptr;
|
||||
static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h
|
||||
#elif ARCH_PORTDUINO
|
||||
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
||||
#if defined(LGFX_SDL)
|
||||
#include <lgfx/v1/platforms/sdl/Panel_sdl.hpp>
|
||||
#endif
|
||||
|
||||
class LGFX : public lgfx::LGFX_Device
|
||||
{
|
||||
lgfx::Panel_Device *_panel_instance;
|
||||
lgfx::Bus_SPI _bus_instance;
|
||||
|
||||
lgfx::ITouch *_touch_instance;
|
||||
|
||||
public:
|
||||
lgfx::Panel_Device *_panel_instance;
|
||||
|
||||
LGFX(void)
|
||||
{
|
||||
if (settingsMap[displayPanel] == st7789)
|
||||
@@ -694,6 +698,11 @@ class LGFX : public lgfx::LGFX_Device
|
||||
_panel_instance = new lgfx::Panel_ILI9488;
|
||||
else if (settingsMap[displayPanel] == hx8357d)
|
||||
_panel_instance = new lgfx::Panel_HX8357D;
|
||||
#if defined(LGFX_SDL)
|
||||
else if (settingsMap[displayPanel] == x11) {
|
||||
_panel_instance = new lgfx::Panel_sdl;
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
_panel_instance = new lgfx::Panel_NULL;
|
||||
LOG_ERROR("Unknown display panel configured!");
|
||||
@@ -754,7 +763,13 @@ class LGFX : public lgfx::LGFX_Device
|
||||
_touch_instance->config(touch_cfg);
|
||||
_panel_instance->setTouch(_touch_instance);
|
||||
}
|
||||
|
||||
#if defined(LGFX_SDL)
|
||||
if (settingsMap[displayPanel] == x11) {
|
||||
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance;
|
||||
sdl_panel_->setup();
|
||||
sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER);
|
||||
}
|
||||
#endif
|
||||
setPanel(_panel_instance); // Sets the panel to use.
|
||||
}
|
||||
};
|
||||
@@ -849,9 +864,29 @@ static LGFX *tft = nullptr;
|
||||
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
|
||||
#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
|
||||
|
||||
class PanelInit_ST7701 : public lgfx::Panel_ST7701
|
||||
{
|
||||
public:
|
||||
const uint8_t *getInitCommands(uint8_t listno) const override
|
||||
{
|
||||
// 180 degree hw rotation: vertical flip, horizontal flip
|
||||
static constexpr const uint8_t list1[] = {0x36, 1, 0x10, // MADCTL for vertical flip
|
||||
0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL
|
||||
0xC7, 1, 0x04, // SDIR: X-direction Control (Horizontal Flip)
|
||||
0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS
|
||||
0xFF, 0xFF};
|
||||
switch (listno) {
|
||||
case 1:
|
||||
return list1;
|
||||
default:
|
||||
return lgfx::Panel_ST7701::getInitCommands(listno);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class LGFX : public lgfx::LGFX_Device
|
||||
{
|
||||
lgfx::Panel_ST7701 _panel_instance;
|
||||
PanelInit_ST7701 _panel_instance;
|
||||
lgfx::Bus_RGB _bus_instance;
|
||||
lgfx::Light_PWM _light_instance;
|
||||
lgfx::Touch_FT5x06 _touch_instance;
|
||||
@@ -1040,6 +1075,49 @@ void TFTDisplay::display(bool fromBlank)
|
||||
}
|
||||
}
|
||||
|
||||
void TFTDisplay::sdlLoop()
|
||||
{
|
||||
#if defined(LGFX_SDL)
|
||||
static int lastPressed = 0;
|
||||
static int shuttingDown = false;
|
||||
if (settingsMap[displayPanel] == x11) {
|
||||
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance;
|
||||
if (sdl_panel_->loop() && !shuttingDown) {
|
||||
LOG_WARN("Window Closed!");
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
}
|
||||
|
||||
// debounce
|
||||
if (lastPressed != 0 && !lgfx::v1::gpio_in(lastPressed))
|
||||
return;
|
||||
if (!lgfx::v1::gpio_in(37)) {
|
||||
lastPressed = 37;
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else if (!lgfx::v1::gpio_in(36)) {
|
||||
lastPressed = 36;
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else if (!lgfx::v1::gpio_in(38)) {
|
||||
lastPressed = 38;
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else if (!lgfx::v1::gpio_in(39)) {
|
||||
lastPressed = 39;
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else if (!lgfx::v1::gpio_in(SDL_SCANCODE_KP_ENTER)) {
|
||||
lastPressed = SDL_SCANCODE_KP_ENTER;
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else {
|
||||
lastPressed = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Send a command to the display (low level function)
|
||||
void TFTDisplay::sendCommand(uint8_t com)
|
||||
{
|
||||
@@ -1184,9 +1262,9 @@ bool TFTDisplay::connect()
|
||||
attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING);
|
||||
#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2)
|
||||
tft->setRotation(1); // T-Deck has the TFT in landscape
|
||||
#elif defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR)
|
||||
#elif defined(T_WATCH_S3)
|
||||
tft->setRotation(2); // T-Watch S3 left-handed orientation
|
||||
#elif ARCH_PORTDUINO
|
||||
#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR)
|
||||
tft->setRotation(0); // use config.yaml to set rotation
|
||||
#else
|
||||
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
|
||||
|
||||
@@ -23,6 +23,7 @@ class TFTDisplay : public OLEDDisplay
|
||||
// Write the buffer to the display memory
|
||||
virtual void display() override { display(false); };
|
||||
virtual void display(bool fromBlank);
|
||||
void sdlLoop();
|
||||
|
||||
// Turn the display upside down
|
||||
virtual void flipScreenVertically();
|
||||
|
||||
738
src/graphics/VirtualKeyboard.cpp
Normal file
738
src/graphics/VirtualKeyboard.cpp
Normal file
@@ -0,0 +1,738 @@
|
||||
#include "VirtualKeyboard.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "main.h"
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis())
|
||||
{
|
||||
initializeKeyboard();
|
||||
// Set cursor to H(2, 5)
|
||||
cursorRow = 2;
|
||||
cursorCol = 5;
|
||||
}
|
||||
|
||||
VirtualKeyboard::~VirtualKeyboard() {}
|
||||
|
||||
void VirtualKeyboard::initializeKeyboard()
|
||||
{
|
||||
// New 4 row, 11 column keyboard layout:
|
||||
static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'},
|
||||
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'},
|
||||
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '},
|
||||
{'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}};
|
||||
|
||||
// Derive layout dimensions and assert they match the configured keyboard grid
|
||||
constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0]));
|
||||
constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0]));
|
||||
static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS");
|
||||
static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS");
|
||||
|
||||
// Initialize all keys to empty first
|
||||
for (int row = 0; row < LAYOUT_ROWS; row++) {
|
||||
for (int col = 0; col < LAYOUT_COLS; col++) {
|
||||
keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0};
|
||||
}
|
||||
}
|
||||
|
||||
// Fill keyboard from the 2D layout
|
||||
for (int row = 0; row < LAYOUT_ROWS; row++) {
|
||||
for (int col = 0; col < LAYOUT_COLS; col++) {
|
||||
char ch = LAYOUT[row][col];
|
||||
// No empty slots in the simplified layout
|
||||
|
||||
VirtualKeyType type = VK_CHAR;
|
||||
if (ch == '\b') {
|
||||
type = VK_BACKSPACE;
|
||||
} else if (ch == '\n') {
|
||||
type = VK_ENTER;
|
||||
} else if (ch == '\x1b') { // ESC
|
||||
type = VK_ESC;
|
||||
} else if (ch == ' ') {
|
||||
type = VK_SPACE;
|
||||
}
|
||||
|
||||
// Make action keys wider to fit text while keeping the last column aligned
|
||||
uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH;
|
||||
keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY)
|
||||
{
|
||||
// Repeat ticking is driven by NotificationRenderer once per frame
|
||||
// Base styles
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// Screen geometry
|
||||
const int screenW = display->getWidth();
|
||||
const int screenH = display->getHeight();
|
||||
|
||||
// Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels
|
||||
// Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide
|
||||
const bool isWide = screenW >= 200;
|
||||
|
||||
// Determine last-column label max width
|
||||
display->setFont(FONT_SMALL);
|
||||
const int wENTER = display->getStringWidth("ENTER");
|
||||
int lastColLabelW = wENTER; // ENTER is usually the widest
|
||||
// Smaller padding on very small screens to avoid excessive whitespace
|
||||
const int lastColPad = (screenW <= 128 ? 2 : 6);
|
||||
const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys
|
||||
|
||||
// Always reserve width for the rightmost text column to avoid overlap on small screens
|
||||
int cellW = 0;
|
||||
int leftoverW = 0;
|
||||
{
|
||||
const int leftCols = KEYBOARD_COLS - 1; // 10 input characters
|
||||
int usableW = screenW - reservedLastColW;
|
||||
if (usableW < leftCols) {
|
||||
// Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely)
|
||||
usableW = leftCols;
|
||||
}
|
||||
cellW = usableW / leftCols;
|
||||
leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right)
|
||||
}
|
||||
|
||||
// Dynamic key geometry
|
||||
int cellH = KEY_HEIGHT;
|
||||
int keyboardStartY = 0;
|
||||
if (screenH <= 64) {
|
||||
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2);
|
||||
const int gapBelowHeader = 0;
|
||||
const int singleLineBoxHeight = FONT_HEIGHT_SMALL;
|
||||
const int gapAboveKeyboard = 0;
|
||||
keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard;
|
||||
if (keyboardStartY < 0)
|
||||
keyboardStartY = 0;
|
||||
if (keyboardStartY > screenH)
|
||||
keyboardStartY = screenH;
|
||||
int keyboardHeight = screenH - keyboardStartY;
|
||||
cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS);
|
||||
} else if (isWide) {
|
||||
// For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width.
|
||||
cellH = std::max((int)KEY_HEIGHT, cellW);
|
||||
|
||||
// Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed.
|
||||
// Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1
|
||||
display->setFont(FONT_SMALL);
|
||||
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1);
|
||||
const int headerToBoxGap = 1;
|
||||
const int gapAboveKb = 1;
|
||||
const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom
|
||||
int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb);
|
||||
int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS;
|
||||
if (maxCellHAllowed < (int)KEY_HEIGHT)
|
||||
maxCellHAllowed = KEY_HEIGHT;
|
||||
if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) {
|
||||
cellH = maxCellHAllowed;
|
||||
}
|
||||
// Keyboard placement from bottom for wide screens
|
||||
int keyboardHeight = KEYBOARD_ROWS * cellH;
|
||||
keyboardStartY = screenH - keyboardHeight;
|
||||
if (keyboardStartY < 0)
|
||||
keyboardStartY = 0;
|
||||
} else {
|
||||
// Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom
|
||||
cellH = KEY_HEIGHT;
|
||||
int keyboardHeight = KEYBOARD_ROWS * cellH;
|
||||
keyboardStartY = screenH - keyboardHeight;
|
||||
if (keyboardStartY < 0)
|
||||
keyboardStartY = 0;
|
||||
}
|
||||
|
||||
// Draw input area above keyboard
|
||||
drawInputArea(display, offsetX, offsetY, keyboardStartY);
|
||||
|
||||
// Precompute per-column x and width with leftover distributed over left columns for even spacing
|
||||
int colX[KEYBOARD_COLS];
|
||||
int colW[KEYBOARD_COLS];
|
||||
int runningX = offsetX;
|
||||
for (int col = 0; col < KEYBOARD_COLS - 1; ++col) {
|
||||
int wcol = cellW + (col < leftoverW ? 1 : 0);
|
||||
colX[col] = runningX;
|
||||
colW[col] = wcol;
|
||||
runningX += wcol;
|
||||
}
|
||||
// Last column
|
||||
colX[KEYBOARD_COLS - 1] = runningX;
|
||||
colW[KEYBOARD_COLS - 1] = reservedLastColW;
|
||||
|
||||
// Draw keyboard grid
|
||||
for (int row = 0; row < KEYBOARD_ROWS; row++) {
|
||||
for (int col = 0; col < KEYBOARD_COLS; col++) {
|
||||
const VirtualKey &k = keyboard[row][col];
|
||||
if (k.character != 0 || k.type != VK_CHAR) {
|
||||
const bool isLastCol = (col == KEYBOARD_COLS - 1);
|
||||
int x = colX[col];
|
||||
int w = colW[col];
|
||||
int y = offsetY + keyboardStartY + row * cellH;
|
||||
int h = cellH;
|
||||
bool selected = (row == cursorRow && col == cursorCol);
|
||||
drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY)
|
||||
{
|
||||
display->setColor(WHITE);
|
||||
|
||||
const int screenWidth = display->getWidth();
|
||||
const int screenHeight = display->getHeight();
|
||||
// Use the standard small font metrics for input box sizing (restore original size)
|
||||
const int inputLineH = FONT_HEIGHT_SMALL;
|
||||
|
||||
// Header uses the standard small (which may be larger on big screens)
|
||||
display->setFont(FONT_SMALL);
|
||||
int headerHeight = 0;
|
||||
if (!headerText.empty()) {
|
||||
// Draw header and reserve exact font height (plus a tighter gap) to maximize input area
|
||||
display->drawString(offsetX + 2, offsetY, headerText.c_str());
|
||||
if (screenHeight <= 64) {
|
||||
headerHeight = FONT_HEIGHT_SMALL - 2; // 11px
|
||||
} else {
|
||||
headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in
|
||||
}
|
||||
}
|
||||
|
||||
const int boxX = offsetX;
|
||||
const int boxWidth = screenWidth;
|
||||
int boxY;
|
||||
int boxHeight;
|
||||
if (screenHeight <= 64) {
|
||||
const int gapBelowHeader = 0;
|
||||
const int fixedBoxHeight = inputLineH;
|
||||
const int gapAboveKeyboard = 0;
|
||||
boxY = offsetY + headerHeight + gapBelowHeader;
|
||||
boxHeight = fixedBoxHeight;
|
||||
if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) {
|
||||
int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY;
|
||||
boxHeight = std::max(1, fixedBoxHeight - over);
|
||||
}
|
||||
} else {
|
||||
const int gapBelowHeader = 1;
|
||||
int gapAboveKeyboard = 1;
|
||||
int tmpBoxY = offsetY + headerHeight + gapBelowHeader;
|
||||
const int minBoxHeight = inputLineH + 2;
|
||||
int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard;
|
||||
if (availableH < minBoxHeight)
|
||||
availableH = minBoxHeight;
|
||||
boxY = tmpBoxY;
|
||||
boxHeight = availableH;
|
||||
}
|
||||
|
||||
// Draw box border
|
||||
display->drawRect(boxX, boxY, boxWidth, boxHeight);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis
|
||||
const int textX = boxX + 2;
|
||||
const int maxTextWidth = boxWidth - 4;
|
||||
const int maxLines = (boxHeight - 2) / inputLineH;
|
||||
if (maxLines >= 2) {
|
||||
// Inner bounds for caret clamping
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
// Wrap text greedily into lines that fit maxTextWidth
|
||||
std::vector<std::string> lines;
|
||||
{
|
||||
std::string remaining = inputText;
|
||||
while (!remaining.empty()) {
|
||||
int bestLen = 0;
|
||||
for (int len = 1; len <= (int)remaining.size(); ++len) {
|
||||
int w = display->getStringWidth(remaining.substr(0, len).c_str());
|
||||
if (w <= maxTextWidth)
|
||||
bestLen = len;
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (bestLen == 0) {
|
||||
// At least show one character to make progress
|
||||
bestLen = 1;
|
||||
}
|
||||
lines.emplace_back(remaining.substr(0, bestLen));
|
||||
remaining.erase(0, bestLen);
|
||||
}
|
||||
}
|
||||
|
||||
const bool scrolledUp = ((int)lines.size() > maxLines);
|
||||
int caretX = textX;
|
||||
int caretY = innerTop;
|
||||
|
||||
// Leave a small top gap to render '...' without replacing the first line
|
||||
const int topInset = 2;
|
||||
const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height
|
||||
int lineY = innerTop + topInset;
|
||||
|
||||
if (scrolledUp) {
|
||||
// Draw three small dots centered horizontally, vertically at the midpoint of the gap
|
||||
// between the inner top and the first line's top baseline. This avoids using a tall glyph.
|
||||
const int firstLineTop = lineY; // baseline top for the first visible line
|
||||
const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested
|
||||
const int centerX = boxX + boxWidth / 2;
|
||||
const int dotSpacing = 3; // px between dots
|
||||
const int dotSize = 1; // small square dot
|
||||
display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize);
|
||||
display->fillRect(centerX, gapMidY, dotSize, dotSize);
|
||||
display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize);
|
||||
}
|
||||
|
||||
// How many lines fit with our top inset and tighter step
|
||||
const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep);
|
||||
const int linesToShow = std::min((int)lines.size(), linesCapacity);
|
||||
const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0;
|
||||
|
||||
for (int i = 0; i < linesToShow; ++i) {
|
||||
const std::string &chunk = lines[startIndex + i];
|
||||
display->drawString(textX, lineY, chunk.c_str());
|
||||
caretX = textX + display->getStringWidth(chunk.c_str());
|
||||
caretY = lineY;
|
||||
lineY += lineStep;
|
||||
}
|
||||
|
||||
// Draw caret at end of the last visible line
|
||||
int caretPadY = 2;
|
||||
if (boxHeight >= inputLineH + 4)
|
||||
caretPadY = 3;
|
||||
int cursorTop = caretY + caretPadY;
|
||||
// Use lineStep so caret height matches the row spacing
|
||||
int cursorH = lineStep - caretPadY * 2;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
// Clamp vertical bounds to stay inside the inner rect
|
||||
if (cursorTop < innerTop)
|
||||
cursorTop = innerTop;
|
||||
if (cursorTop + cursorH - 1 > innerBottom)
|
||||
cursorH = innerBottom - cursorTop + 1;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
// Only draw if cursor is inside inner bounds
|
||||
if (caretX >= innerLeft && caretX <= innerRight) {
|
||||
display->drawVerticalLine(caretX, cursorTop, cursorH);
|
||||
}
|
||||
} else {
|
||||
std::string displayText = inputText;
|
||||
int textW = display->getStringWidth(displayText.c_str());
|
||||
std::string scrolled = displayText;
|
||||
if (textW > maxTextWidth) {
|
||||
// Trim from the left until it fits
|
||||
while (textW > maxTextWidth && !scrolled.empty()) {
|
||||
scrolled.erase(0, 1);
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
}
|
||||
// Add leading ellipsis and ensure it still fits
|
||||
if (scrolled != displayText) {
|
||||
scrolled = "..." + scrolled;
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
// If adding ellipsis causes overflow, trim more after the ellipsis
|
||||
while (textW > maxTextWidth && scrolled.size() > 3) {
|
||||
scrolled.erase(3, 1); // remove chars after the ellipsis
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Keep textW in sync with what we draw
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
}
|
||||
|
||||
int textY;
|
||||
if (screenHeight <= 64) {
|
||||
textY = boxY + (boxHeight - inputLineH) / 2;
|
||||
} else {
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
// Center text vertically within inner box for single-line, then clamp so it never overlaps borders
|
||||
int innerH = innerBottom - innerTop + 1;
|
||||
textY = innerTop + std::max(0, (innerH - inputLineH) / 2);
|
||||
// Clamp fully inside the inner rect
|
||||
if (textY < innerTop)
|
||||
textY = innerTop;
|
||||
int maxTop = innerBottom - inputLineH + 1;
|
||||
if (textY > maxTop)
|
||||
textY = maxTop;
|
||||
}
|
||||
|
||||
if (!scrolled.empty()) {
|
||||
display->drawString(textX, textY, scrolled.c_str());
|
||||
}
|
||||
|
||||
int cursorX = textX + textW;
|
||||
if (screenHeight > 64) {
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
if (cursorX > innerRight)
|
||||
cursorX = innerRight;
|
||||
}
|
||||
|
||||
int cursorTop, cursorH;
|
||||
if (screenHeight <= 64) {
|
||||
cursorH = 10;
|
||||
cursorTop = boxY + (boxHeight - cursorH) / 2;
|
||||
} else {
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
cursorTop = boxY + 2;
|
||||
cursorH = boxHeight - 4;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
if (cursorTop < innerTop)
|
||||
cursorTop = innerTop;
|
||||
if (cursorTop + cursorH - 1 > innerBottom)
|
||||
cursorH = innerBottom - cursorTop + 1;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
|
||||
if (cursorX < innerLeft || cursorX > innerRight)
|
||||
return;
|
||||
}
|
||||
|
||||
display->drawVerticalLine(cursorX, cursorTop, cursorH);
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width,
|
||||
uint8_t height, bool isLastCol)
|
||||
{
|
||||
// Draw key content
|
||||
display->setFont(FONT_SMALL);
|
||||
const int fontH = FONT_HEIGHT_SMALL;
|
||||
// Build label and metrics first
|
||||
std::string keyText;
|
||||
if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) {
|
||||
// Keep literal text labels for the action keys on the rightmost column
|
||||
keyText = (key.type == VK_BACKSPACE) ? "BACK"
|
||||
: (key.type == VK_ENTER) ? "ENTER"
|
||||
: (key.type == VK_SPACE) ? "SPACE"
|
||||
: (key.type == VK_ESC) ? "ESC"
|
||||
: "";
|
||||
} else {
|
||||
char c = getCharForKey(key, false);
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c = c - 'a' + 'A';
|
||||
}
|
||||
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
|
||||
}
|
||||
|
||||
int textWidth = display->getStringWidth(keyText.c_str());
|
||||
// Label alignment
|
||||
// - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly.
|
||||
// - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths.
|
||||
int textX;
|
||||
if (isLastCol) {
|
||||
const int rightPad = 1;
|
||||
textX = x + width - textWidth - rightPad;
|
||||
if (textX < x)
|
||||
textX = x; // guard
|
||||
} else {
|
||||
if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) {
|
||||
textX = x + (width - textWidth + 1) / 2;
|
||||
} else {
|
||||
textX = x + (width - textWidth) / 2;
|
||||
}
|
||||
}
|
||||
int contentTop = y;
|
||||
int contentH = height;
|
||||
if (selected) {
|
||||
display->setColor(WHITE);
|
||||
bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC);
|
||||
|
||||
if (display->getHeight() <= 64 && !isAction) {
|
||||
display->fillRect(x, y, width, height);
|
||||
} else if (isAction) {
|
||||
const int padX = 1;
|
||||
const int padY = 2;
|
||||
int hlW = textWidth + padX * 2;
|
||||
int hlX = textX - padX;
|
||||
|
||||
if (hlX < x) {
|
||||
hlW -= (x - hlX);
|
||||
hlX = x;
|
||||
}
|
||||
int maxW = (x + width) - hlX;
|
||||
if (hlW > maxW)
|
||||
hlW = maxW;
|
||||
if (hlW < 1)
|
||||
hlW = 1;
|
||||
|
||||
int hlH = std::min(fontH + padY * 2, (int)height);
|
||||
int hlY = y + (height - hlH) / 2;
|
||||
display->fillRect(hlX, hlY, hlW, hlH);
|
||||
contentTop = hlY;
|
||||
contentH = hlH;
|
||||
} else {
|
||||
display->fillRect(x, y, width, height);
|
||||
}
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
|
||||
int centeredTextY;
|
||||
if (display->getHeight() <= 64) {
|
||||
centeredTextY = y + (height - fontH) / 2;
|
||||
} else {
|
||||
centeredTextY = contentTop + (contentH - fontH) / 2;
|
||||
}
|
||||
if (display->getHeight() > 64) {
|
||||
if (centeredTextY < contentTop)
|
||||
centeredTextY = contentTop;
|
||||
if (centeredTextY + fontH > contentTop + contentH)
|
||||
centeredTextY = std::max(contentTop, contentTop + contentH - fontH);
|
||||
}
|
||||
|
||||
if (display->getHeight() <= 64 && keyText.size() == 1) {
|
||||
char ch = keyText[0];
|
||||
if (ch == '.' || ch == ',' || ch == ';') {
|
||||
centeredTextY -= 1;
|
||||
}
|
||||
}
|
||||
display->drawString(textX, centeredTextY, keyText.c_str());
|
||||
}
|
||||
|
||||
char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
|
||||
{
|
||||
if (key.type != VK_CHAR) {
|
||||
return key.character;
|
||||
}
|
||||
|
||||
char c = key.character;
|
||||
|
||||
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
|
||||
if (isLongPress && c >= 'a' && c <= 'z') {
|
||||
c = (char)(c - 'a' + 'A');
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::moveCursorDelta(int dRow, int dCol)
|
||||
{
|
||||
resetTimeout();
|
||||
// wrap around rows and cols in the 4x11 grid
|
||||
int r = (int)cursorRow + dRow;
|
||||
int c = (int)cursorCol + dCol;
|
||||
if (r < 0)
|
||||
r = KEYBOARD_ROWS - 1;
|
||||
else if (r >= KEYBOARD_ROWS)
|
||||
r = 0;
|
||||
if (c < 0)
|
||||
c = KEYBOARD_COLS - 1;
|
||||
else if (c >= KEYBOARD_COLS)
|
||||
c = 0;
|
||||
cursorRow = (uint8_t)r;
|
||||
cursorCol = (uint8_t)c;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::moveCursorUp()
|
||||
{
|
||||
moveCursorDelta(-1, 0);
|
||||
}
|
||||
void VirtualKeyboard::moveCursorDown()
|
||||
{
|
||||
moveCursorDelta(1, 0);
|
||||
}
|
||||
void VirtualKeyboard::moveCursorLeft()
|
||||
{
|
||||
resetTimeout();
|
||||
|
||||
if (cursorCol > 0) {
|
||||
cursorCol--;
|
||||
} else {
|
||||
if (cursorRow > 0) {
|
||||
cursorRow--;
|
||||
cursorCol = KEYBOARD_COLS - 1;
|
||||
} else {
|
||||
cursorRow = KEYBOARD_ROWS - 1;
|
||||
cursorCol = KEYBOARD_COLS - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
void VirtualKeyboard::moveCursorRight()
|
||||
{
|
||||
resetTimeout();
|
||||
|
||||
if (cursorCol < KEYBOARD_COLS - 1) {
|
||||
cursorCol++;
|
||||
} else {
|
||||
if (cursorRow < KEYBOARD_ROWS - 1) {
|
||||
cursorRow++;
|
||||
cursorCol = 0;
|
||||
} else {
|
||||
cursorRow = 0;
|
||||
cursorCol = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::handlePress()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
const VirtualKey &key = keyboard[cursorRow][cursorCol];
|
||||
|
||||
// Don't handle press if the key is empty (but allow special keys)
|
||||
if (key.character == 0 && key.type == VK_CHAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For character keys, insert lowercase character
|
||||
if (key.type == VK_CHAR) {
|
||||
insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle non-character keys immediately
|
||||
switch (key.type) {
|
||||
case VK_BACKSPACE:
|
||||
deleteCharacter();
|
||||
break;
|
||||
case VK_ENTER:
|
||||
submitText();
|
||||
break;
|
||||
case VK_SPACE:
|
||||
insertCharacter(' ');
|
||||
break;
|
||||
case VK_ESC:
|
||||
if (onTextEntered) {
|
||||
std::function<void(const std::string &)> callback = onTextEntered;
|
||||
onTextEntered = nullptr;
|
||||
inputText = "";
|
||||
callback("");
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::handleLongPress()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
const VirtualKey &key = keyboard[cursorRow][cursorCol];
|
||||
|
||||
// Don't handle press if the key is empty (but allow special keys)
|
||||
if (key.character == 0 && key.type == VK_CHAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For character keys, insert uppercase/alternate character
|
||||
if (key.type == VK_CHAR) {
|
||||
insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key.type) {
|
||||
case VK_BACKSPACE:
|
||||
// One-shot: delete up to 5 characters on long press
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
if (inputText.empty())
|
||||
break;
|
||||
deleteCharacter();
|
||||
}
|
||||
break;
|
||||
case VK_ENTER:
|
||||
submitText();
|
||||
break;
|
||||
case VK_SPACE:
|
||||
insertCharacter(' ');
|
||||
break;
|
||||
case VK_ESC:
|
||||
if (onTextEntered) {
|
||||
onTextEntered("");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::insertCharacter(char c)
|
||||
{
|
||||
if (inputText.length() < 160) { // Reasonable text length limit
|
||||
inputText += c;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::deleteCharacter()
|
||||
{
|
||||
if (!inputText.empty()) {
|
||||
inputText.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::submitText()
|
||||
{
|
||||
LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str());
|
||||
|
||||
// Only submit if text is not empty
|
||||
if (!inputText.empty() && onTextEntered) {
|
||||
// Store callback and text to submit before clearing callback
|
||||
std::function<void(const std::string &)> callback = onTextEntered;
|
||||
std::string textToSubmit = inputText;
|
||||
onTextEntered = nullptr;
|
||||
// Don't clear inputText here - let the calling module handle cleanup
|
||||
// inputText = ""; // Removed: keep text visible until module cleans up
|
||||
callback(textToSubmit);
|
||||
} else if (inputText.empty()) {
|
||||
// For empty text, just ignore the submission - don't clear callback
|
||||
// This keeps the virtual keyboard responsive for further input
|
||||
LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active");
|
||||
} else {
|
||||
// No callback available
|
||||
if (screen) {
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setInputText(const std::string &text)
|
||||
{
|
||||
inputText = text;
|
||||
}
|
||||
|
||||
std::string VirtualKeyboard::getInputText() const
|
||||
{
|
||||
return inputText;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setHeader(const std::string &header)
|
||||
{
|
||||
headerText = header;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setCallback(std::function<void(const std::string &)> callback)
|
||||
{
|
||||
onTextEntered = callback;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::resetTimeout()
|
||||
{
|
||||
lastActivityTime = millis();
|
||||
}
|
||||
|
||||
bool VirtualKeyboard::isTimedOut() const
|
||||
{
|
||||
return (millis() - lastActivityTime) > TIMEOUT_MS;
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
80
src/graphics/VirtualKeyboard.h
Normal file
80
src/graphics/VirtualKeyboard.h
Normal file
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE };
|
||||
|
||||
struct VirtualKey {
|
||||
char character;
|
||||
VirtualKeyType type;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
};
|
||||
|
||||
class VirtualKeyboard
|
||||
{
|
||||
public:
|
||||
VirtualKeyboard();
|
||||
~VirtualKeyboard();
|
||||
|
||||
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
|
||||
void setInputText(const std::string &text);
|
||||
std::string getInputText() const;
|
||||
void setHeader(const std::string &header);
|
||||
void setCallback(std::function<void(const std::string &)> callback);
|
||||
|
||||
// Navigation methods for encoder input
|
||||
void moveCursorUp();
|
||||
void moveCursorDown();
|
||||
void moveCursorLeft();
|
||||
void moveCursorRight();
|
||||
void handlePress();
|
||||
void handleLongPress();
|
||||
|
||||
// Timeout management
|
||||
void resetTimeout();
|
||||
bool isTimedOut() const;
|
||||
|
||||
private:
|
||||
static const uint8_t KEYBOARD_ROWS = 4;
|
||||
static const uint8_t KEYBOARD_COLS = 11;
|
||||
static const uint8_t KEY_WIDTH = 9;
|
||||
static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays
|
||||
static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom
|
||||
|
||||
VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS];
|
||||
|
||||
std::string inputText;
|
||||
std::string headerText;
|
||||
std::function<void(const std::string &)> onTextEntered;
|
||||
|
||||
uint8_t cursorRow;
|
||||
uint8_t cursorCol;
|
||||
|
||||
// Timeout management for auto-exit
|
||||
uint32_t lastActivityTime;
|
||||
static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout
|
||||
|
||||
void initializeKeyboard();
|
||||
void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h,
|
||||
bool isLastCol);
|
||||
void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY);
|
||||
|
||||
// Unified cursor movement helper
|
||||
void moveCursorDelta(int dRow, int dCol);
|
||||
|
||||
char getCharForKey(const VirtualKey &key, bool isLongPress = false);
|
||||
void insertCharacter(char c);
|
||||
void deleteCharacter();
|
||||
void submitText();
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
@@ -10,7 +10,10 @@
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "main.h"
|
||||
#include "mesh/MeshTypes.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#include "modules/KeyVerificationModule.h"
|
||||
|
||||
@@ -7,10 +7,18 @@
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/images.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#if HAS_BUTTON
|
||||
#include "input/ButtonThread.h"
|
||||
#endif
|
||||
#include "main.h"
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#if HAS_TRACKBALL
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
#include "esp_task_wdt.h"
|
||||
@@ -18,6 +26,11 @@
|
||||
|
||||
using namespace meshtastic;
|
||||
|
||||
#if HAS_BUTTON
|
||||
// Global button thread pointer defined in main.cpp
|
||||
extern ::ButtonThread *UserButtonThread;
|
||||
#endif
|
||||
|
||||
// External references to global variables from Screen.cpp
|
||||
extern std::vector<std::string> functionSymbol;
|
||||
extern std::string functionSymbolString;
|
||||
@@ -38,6 +51,8 @@ bool NotificationRenderer::pauseBanner = false;
|
||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||
uint32_t NotificationRenderer::numDigits = 0;
|
||||
uint32_t NotificationRenderer::currentNumber = 0;
|
||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
uint32_t pow_of_10(uint32_t n)
|
||||
{
|
||||
@@ -89,14 +104,33 @@ void NotificationRenderer::resetBanner()
|
||||
|
||||
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0')
|
||||
resetBanner();
|
||||
if (!isOverlayBannerShowing() || pauseBanner)
|
||||
// Handle text_input notifications first - they have their own timeout/banner logic
|
||||
if (current_notification_type == notificationTypeEnum::text_input) {
|
||||
// Check for timeout and reset if needed for text input
|
||||
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
drawTextInput(display, state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||
resetBanner();
|
||||
}
|
||||
|
||||
// Exit if no banner is showing or banner is paused
|
||||
if (!isOverlayBannerShowing() || pauseBanner) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (current_notification_type) {
|
||||
case notificationTypeEnum::none:
|
||||
// Do nothing - no notification to display
|
||||
break;
|
||||
case notificationTypeEnum::text_input:
|
||||
// Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch.
|
||||
break;
|
||||
case notificationTypeEnum::text_banner:
|
||||
case notificationTypeEnum::selection_picker:
|
||||
drawAlertBannerOverlay(display, state);
|
||||
@@ -267,12 +301,9 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
|
||||
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
|
||||
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
|
||||
|
||||
} else {
|
||||
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
|
||||
}
|
||||
// make temp buffer for name
|
||||
// fi
|
||||
if (i == curSelected) {
|
||||
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
|
||||
if (isHighResolution) {
|
||||
@@ -286,7 +317,8 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
}
|
||||
scratchLineBuffer[scratchLineNum][39] = '\0';
|
||||
} else {
|
||||
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36);
|
||||
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39);
|
||||
scratchLineBuffer[scratchLineNum][39] = '\0';
|
||||
}
|
||||
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
|
||||
}
|
||||
@@ -575,6 +607,100 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi
|
||||
"Please be patient and do not power off.");
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
if (virtualKeyboard) {
|
||||
// Check for timeout and auto-exit if needed
|
||||
if (virtualKeyboard->isTimedOut()) {
|
||||
LOG_INFO("Virtual keyboard timeout - auto-exiting");
|
||||
// Cancel virtual keyboard - call callback with empty string to indicate timeout
|
||||
auto callback = textInputCallback; // Store callback before clearing
|
||||
|
||||
// Clean up first to prevent re-entry
|
||||
delete virtualKeyboard;
|
||||
virtualKeyboard = nullptr;
|
||||
textInputCallback = nullptr;
|
||||
resetBanner();
|
||||
|
||||
// Call callback after cleanup
|
||||
if (callback) {
|
||||
callback("");
|
||||
}
|
||||
|
||||
// Restore normal overlays
|
||||
if (screen) {
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP) {
|
||||
// high frequency for move cursor left/right than up/down with encoders
|
||||
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
||||
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
|
||||
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
|
||||
virtualKeyboard->moveCursorLeft();
|
||||
} else {
|
||||
virtualKeyboard->moveCursorUp();
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN) {
|
||||
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
||||
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
|
||||
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
|
||||
virtualKeyboard->moveCursorRight();
|
||||
} else {
|
||||
virtualKeyboard->moveCursorDown();
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||
virtualKeyboard->moveCursorLeft();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||
virtualKeyboard->moveCursorRight();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
virtualKeyboard->moveCursorUp();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
virtualKeyboard->moveCursorDown();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||
// Long press UP = move left
|
||||
virtualKeyboard->moveCursorLeft();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||
// Long press DOWN = move right
|
||||
virtualKeyboard->moveCursorRight();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
|
||||
virtualKeyboard->handlePress();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) {
|
||||
virtualKeyboard->handleLongPress();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) {
|
||||
auto callback = textInputCallback;
|
||||
delete virtualKeyboard;
|
||||
virtualKeyboard = nullptr;
|
||||
textInputCallback = nullptr;
|
||||
resetBanner();
|
||||
if (callback) {
|
||||
callback("");
|
||||
}
|
||||
if (screen) {
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume the event after processing for virtual keyboard
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
}
|
||||
|
||||
// Clear the screen to avoid overlapping with underlying frames or overlays
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, display->getWidth(), display->getHeight());
|
||||
display->setColor(WHITE);
|
||||
virtualKeyboard->draw(display, 0, 0);
|
||||
} else {
|
||||
// If virtualKeyboard is null, reset the banner to avoid getting stuck
|
||||
LOG_INFO("Virtual keyboard is null - resetting banner");
|
||||
resetBanner();
|
||||
}
|
||||
}
|
||||
|
||||
bool NotificationRenderer::isOverlayBannerShowing()
|
||||
{
|
||||
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "OLEDDisplay.h"
|
||||
#include "OLEDDisplayUi.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/VirtualKeyboard.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#define MAX_LINES 5
|
||||
|
||||
namespace graphics
|
||||
@@ -22,6 +25,8 @@ class NotificationRenderer
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
static VirtualKeyboard *virtualKeyboard;
|
||||
static std::function<void(const std::string &)> textInputCallback;
|
||||
|
||||
static bool pauseBanner;
|
||||
|
||||
@@ -30,6 +35,7 @@ class NotificationRenderer
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
|
||||
|
||||
|
||||
@@ -76,6 +76,9 @@ class ButtonThread : public Observable<const InputEvent *>, public concurrency::
|
||||
return digitalRead(buttonPin); // Most buttons are active low by default
|
||||
}
|
||||
|
||||
// Returns true while this thread's button is physically held down
|
||||
bool isHeld() { return isButtonPressed(_pinNum); }
|
||||
|
||||
// Disconnect and reconnect interrupts for light sleep
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused);
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
enum input_broker_event {
|
||||
INPUT_BROKER_NONE = 0,
|
||||
INPUT_BROKER_SELECT = 10,
|
||||
INPUT_BROKER_SELECT_LONG = 11,
|
||||
INPUT_BROKER_UP_LONG = 12,
|
||||
INPUT_BROKER_DOWN_LONG = 13,
|
||||
INPUT_BROKER_UP = 17,
|
||||
INPUT_BROKER_DOWN = 18,
|
||||
INPUT_BROKER_LEFT = 19,
|
||||
|
||||
@@ -8,22 +8,24 @@ RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concu
|
||||
|
||||
void RotaryEncoderInterruptBase::init(
|
||||
uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
|
||||
input_broker_event eventPressed,
|
||||
input_broker_event eventPressed, input_broker_event eventPressedLong,
|
||||
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress) :
|
||||
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)())
|
||||
{
|
||||
this->_pinA = pinA;
|
||||
this->_pinB = pinB;
|
||||
this->_pinPress = pinPress;
|
||||
this->_eventCw = eventCw;
|
||||
this->_eventCcw = eventCcw;
|
||||
this->_eventPressed = eventPressed;
|
||||
this->_eventPressedLong = eventPressedLong;
|
||||
|
||||
pinMode(pinPress, INPUT_PULLUP);
|
||||
pinMode(this->_pinA, INPUT_PULLUP);
|
||||
pinMode(this->_pinB, INPUT_PULLUP);
|
||||
|
||||
// attachInterrupt(pinPress, onIntPress, RISING);
|
||||
attachInterrupt(pinPress, onIntPress, RISING);
|
||||
// Use FALLING edge for active-low press button to start long-press timing immediately
|
||||
attachInterrupt(pinPress, onIntPress, FALLING);
|
||||
attachInterrupt(this->_pinA, onIntA, CHANGE);
|
||||
attachInterrupt(this->_pinB, onIntB, CHANGE);
|
||||
|
||||
@@ -37,10 +39,37 @@ int32_t RotaryEncoderInterruptBase::runOnce()
|
||||
InputEvent e;
|
||||
e.inputEvent = INPUT_BROKER_NONE;
|
||||
e.source = this->_originName;
|
||||
unsigned long now = millis();
|
||||
|
||||
// Handle press long/short detection
|
||||
if (this->action == ROTARY_ACTION_PRESSED) {
|
||||
LOG_DEBUG("Rotary event Press");
|
||||
e.inputEvent = this->_eventPressed;
|
||||
bool buttonPressed = !digitalRead(_pinPress);
|
||||
if (!pressDetected && buttonPressed) {
|
||||
pressDetected = true;
|
||||
pressStartTime = now;
|
||||
}
|
||||
|
||||
if (pressDetected) {
|
||||
uint32_t duration = now - pressStartTime;
|
||||
if (!buttonPressed) {
|
||||
// released -> if short press, send short, else already sent long
|
||||
if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
|
||||
lastPressKeyTime = now;
|
||||
LOG_DEBUG("Rotary event Press short");
|
||||
e.inputEvent = this->_eventPressed;
|
||||
}
|
||||
pressDetected = false;
|
||||
pressStartTime = 0;
|
||||
lastPressLongEventTime = 0;
|
||||
this->action = ROTARY_ACTION_NONE;
|
||||
} else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE &&
|
||||
lastPressLongEventTime == 0) {
|
||||
// fire single-shot long press
|
||||
lastPressLongEventTime = now;
|
||||
LOG_DEBUG("Rotary event Press long");
|
||||
e.inputEvent = this->_eventPressedLong;
|
||||
}
|
||||
}
|
||||
} else if (this->action == ROTARY_ACTION_CW) {
|
||||
LOG_DEBUG("Rotary event CW");
|
||||
e.inputEvent = this->_eventCw;
|
||||
@@ -53,7 +82,9 @@ int32_t RotaryEncoderInterruptBase::runOnce()
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
|
||||
this->action = ROTARY_ACTION_NONE;
|
||||
if (!pressDetected) {
|
||||
this->action = ROTARY_ACTION_NONE;
|
||||
}
|
||||
|
||||
return INT32_MAX;
|
||||
}
|
||||
@@ -61,7 +92,7 @@ int32_t RotaryEncoderInterruptBase::runOnce()
|
||||
void RotaryEncoderInterruptBase::intPressHandler()
|
||||
{
|
||||
this->action = ROTARY_ACTION_PRESSED;
|
||||
setIntervalFromNow(20); // TODO: this modifies a non-volatile variable!
|
||||
setIntervalFromNow(20); // start checking for long/short
|
||||
}
|
||||
|
||||
void RotaryEncoderInterruptBase::intAHandler()
|
||||
|
||||
@@ -13,7 +13,7 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
|
||||
public:
|
||||
explicit RotaryEncoderInterruptBase(const char *name);
|
||||
void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
|
||||
input_broker_event eventPressed,
|
||||
input_broker_event eventPressed, input_broker_event eventPressedLong,
|
||||
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress);
|
||||
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)());
|
||||
void intPressHandler();
|
||||
@@ -33,10 +33,22 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
|
||||
volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE;
|
||||
|
||||
private:
|
||||
// pins and events
|
||||
uint8_t _pinA = 0;
|
||||
uint8_t _pinB = 0;
|
||||
uint8_t _pinPress = 0;
|
||||
input_broker_event _eventCw = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventCcw = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventPressed = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
|
||||
const char *_originName;
|
||||
|
||||
// Long press detection variables
|
||||
uint32_t pressStartTime = 0;
|
||||
bool pressDetected = false;
|
||||
uint32_t lastPressLongEventTime = 0;
|
||||
unsigned long lastPressKeyTime = 0;
|
||||
static const uint32_t LONG_PRESS_DURATION = 300; // ms
|
||||
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 0; // 0 = single-shot for rotary select
|
||||
const unsigned long pressDebounceMs = 200; // ms
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "RotaryEncoderInterruptImpl1.h"
|
||||
#include "InputBroker.h"
|
||||
extern bool osk_found;
|
||||
|
||||
RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
||||
|
||||
@@ -19,12 +20,14 @@ bool RotaryEncoderInterruptImpl1::init()
|
||||
input_broker_event eventCw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_cw);
|
||||
input_broker_event eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw);
|
||||
input_broker_event eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
|
||||
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
|
||||
|
||||
// moduleConfig.canned_message.ext_notification_module_output
|
||||
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed,
|
||||
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong,
|
||||
RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB,
|
||||
RotaryEncoderInterruptImpl1::handleIntPressed);
|
||||
inputBroker->registerSource(this);
|
||||
osk_found = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#include "TrackballInterruptBase.h"
|
||||
#include "configuration.h"
|
||||
extern bool osk_found;
|
||||
|
||||
TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {}
|
||||
|
||||
void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress,
|
||||
input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft,
|
||||
input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(),
|
||||
void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)())
|
||||
input_broker_event eventRight, input_broker_event eventPressed,
|
||||
input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(),
|
||||
void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)())
|
||||
{
|
||||
this->_pinDown = pinDown;
|
||||
this->_pinUp = pinUp;
|
||||
@@ -18,6 +20,7 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
|
||||
this->_eventLeft = eventLeft;
|
||||
this->_eventRight = eventRight;
|
||||
this->_eventPressed = eventPressed;
|
||||
this->_eventPressedLong = eventPressedLong;
|
||||
|
||||
if (pinPress != 255) {
|
||||
pinMode(pinPress, INPUT_PULLUP);
|
||||
@@ -40,9 +43,9 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
|
||||
attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION);
|
||||
}
|
||||
|
||||
LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight,
|
||||
pinPress);
|
||||
|
||||
LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown,
|
||||
this->_pinLeft, this->_pinRight, pinPress);
|
||||
osk_found = true;
|
||||
this->setInterval(100);
|
||||
}
|
||||
|
||||
@@ -50,10 +53,47 @@ int32_t TrackballInterruptBase::runOnce()
|
||||
{
|
||||
InputEvent e;
|
||||
e.inputEvent = INPUT_BROKER_NONE;
|
||||
|
||||
// Handle long press detection for press button
|
||||
if (pressDetected && pressStartTime > 0) {
|
||||
uint32_t pressDuration = millis() - pressStartTime;
|
||||
bool buttonStillPressed = false;
|
||||
|
||||
#if defined(T_DECK)
|
||||
buttonStillPressed = (this->action == TB_ACTION_PRESSED);
|
||||
#else
|
||||
buttonStillPressed = !digitalRead(_pinPress);
|
||||
#endif
|
||||
|
||||
if (!buttonStillPressed) {
|
||||
// Button released
|
||||
if (pressDuration < LONG_PRESS_DURATION) {
|
||||
// Short press
|
||||
e.inputEvent = this->_eventPressed;
|
||||
}
|
||||
// Reset state
|
||||
pressDetected = false;
|
||||
pressStartTime = 0;
|
||||
lastLongPressEventTime = 0;
|
||||
this->action = TB_ACTION_NONE;
|
||||
} else if (pressDuration >= LONG_PRESS_DURATION) {
|
||||
// Long press detected
|
||||
uint32_t currentTime = millis();
|
||||
// Only trigger long press event if enough time has passed since the last one
|
||||
if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
|
||||
e.inputEvent = this->_eventPressedLong;
|
||||
lastLongPressEventTime = currentTime;
|
||||
}
|
||||
this->action = TB_ACTION_PRESSED_LONG;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
|
||||
if (this->action == TB_ACTION_PRESSED) {
|
||||
// LOG_DEBUG("Trackball event Press");
|
||||
e.inputEvent = this->_eventPressed;
|
||||
if (this->action == TB_ACTION_PRESSED && !pressDetected) {
|
||||
// Start long press detection
|
||||
pressDetected = true;
|
||||
pressStartTime = millis();
|
||||
// Don't send event yet, wait to see if it's a long press
|
||||
} else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) {
|
||||
// LOG_DEBUG("Trackball event UP");
|
||||
e.inputEvent = this->_eventUp;
|
||||
@@ -68,9 +108,11 @@ int32_t TrackballInterruptBase::runOnce()
|
||||
e.inputEvent = this->_eventRight;
|
||||
}
|
||||
#else
|
||||
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) {
|
||||
// LOG_DEBUG("Trackball event Press");
|
||||
e.inputEvent = this->_eventPressed;
|
||||
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) {
|
||||
// Start long press detection
|
||||
pressDetected = true;
|
||||
pressStartTime = millis();
|
||||
// Don't send event yet, wait to see if it's a long press
|
||||
} else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) {
|
||||
// LOG_DEBUG("Trackball event UP");
|
||||
e.inputEvent = this->_eventUp;
|
||||
@@ -91,10 +133,16 @@ int32_t TrackballInterruptBase::runOnce()
|
||||
e.kbchar = 0x00;
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
lastEvent = action;
|
||||
this->action = TB_ACTION_NONE;
|
||||
|
||||
return 100;
|
||||
// Only update lastEvent for non-press actions or completed press actions
|
||||
if (this->action != TB_ACTION_PRESSED || !pressDetected) {
|
||||
lastEvent = action;
|
||||
if (!pressDetected) {
|
||||
this->action = TB_ACTION_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
return 50; // Check more frequently for better long press detection
|
||||
}
|
||||
|
||||
void TrackballInterruptBase::intPressHandler()
|
||||
|
||||
@@ -18,8 +18,8 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
|
||||
explicit TrackballInterruptBase(const char *name);
|
||||
void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown,
|
||||
input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight,
|
||||
input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(),
|
||||
void (*onIntPress)());
|
||||
input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(),
|
||||
void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)());
|
||||
void intPressHandler();
|
||||
void intDownHandler();
|
||||
void intUpHandler();
|
||||
@@ -33,6 +33,7 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
|
||||
enum TrackballInterruptBaseActionType {
|
||||
TB_ACTION_NONE,
|
||||
TB_ACTION_PRESSED,
|
||||
TB_ACTION_PRESSED_LONG,
|
||||
TB_ACTION_UP,
|
||||
TB_ACTION_DOWN,
|
||||
TB_ACTION_LEFT,
|
||||
@@ -46,12 +47,20 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
|
||||
|
||||
volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE;
|
||||
|
||||
// Long press detection for press button
|
||||
uint32_t pressStartTime = 0;
|
||||
bool pressDetected = false;
|
||||
uint32_t lastLongPressEventTime = 0;
|
||||
static const uint32_t LONG_PRESS_DURATION = 500; // ms
|
||||
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events
|
||||
|
||||
private:
|
||||
input_broker_event _eventDown = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventUp = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventLeft = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventRight = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventPressed = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
|
||||
const char *_originName;
|
||||
TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE;
|
||||
};
|
||||
|
||||
@@ -13,11 +13,12 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe
|
||||
input_broker_event eventLeft = INPUT_BROKER_LEFT;
|
||||
input_broker_event eventRight = INPUT_BROKER_RIGHT;
|
||||
input_broker_event eventPressed = INPUT_BROKER_SELECT;
|
||||
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
|
||||
|
||||
TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight,
|
||||
eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp,
|
||||
TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight,
|
||||
TrackballInterruptImpl1::handleIntPressed);
|
||||
eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown,
|
||||
TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft,
|
||||
TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed);
|
||||
inputBroker->registerSource(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,26 +7,36 @@ UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThre
|
||||
}
|
||||
|
||||
void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown,
|
||||
input_broker_event eventUp, input_broker_event eventPressed, void (*onIntDown)(),
|
||||
input_broker_event eventUp, input_broker_event eventPressed, input_broker_event eventPressedLong,
|
||||
input_broker_event eventUpLong, input_broker_event eventDownLong, void (*onIntDown)(),
|
||||
void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs)
|
||||
{
|
||||
this->_pinDown = pinDown;
|
||||
this->_pinUp = pinUp;
|
||||
this->_pinPress = pinPress;
|
||||
this->_eventDown = eventDown;
|
||||
this->_eventUp = eventUp;
|
||||
this->_eventPressed = eventPressed;
|
||||
this->_eventPressedLong = eventPressedLong;
|
||||
this->_eventUpLong = eventUpLong;
|
||||
this->_eventDownLong = eventDownLong;
|
||||
|
||||
// Store debounce configuration passed by caller
|
||||
this->updownDebounceMs = updownDebounceMs;
|
||||
|
||||
pinMode(pinPress, INPUT_PULLUP);
|
||||
pinMode(this->_pinDown, INPUT_PULLUP);
|
||||
pinMode(this->_pinUp, INPUT_PULLUP);
|
||||
|
||||
attachInterrupt(pinPress, onIntPress, RISING);
|
||||
attachInterrupt(this->_pinDown, onIntDown, RISING);
|
||||
attachInterrupt(this->_pinUp, onIntUp, RISING);
|
||||
// Use FALLING edge for active-low buttons so we detect press at the moment of pressing
|
||||
// This enables long-press timing to start immediately instead of waiting for release.
|
||||
attachInterrupt(pinPress, onIntPress, FALLING);
|
||||
attachInterrupt(this->_pinDown, onIntDown, FALLING);
|
||||
attachInterrupt(this->_pinUp, onIntUp, FALLING);
|
||||
|
||||
LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress);
|
||||
|
||||
this->setInterval(100);
|
||||
this->setInterval(20);
|
||||
}
|
||||
|
||||
int32_t UpDownInterruptBase::runOnce()
|
||||
@@ -34,23 +44,88 @@ int32_t UpDownInterruptBase::runOnce()
|
||||
InputEvent e;
|
||||
e.inputEvent = INPUT_BROKER_NONE;
|
||||
unsigned long now = millis();
|
||||
if (this->action == UPDOWN_ACTION_PRESSED) {
|
||||
if (now - lastPressKeyTime >= pressDebounceMs) {
|
||||
lastPressKeyTime = now;
|
||||
LOG_DEBUG("GPIO event Press");
|
||||
e.inputEvent = this->_eventPressed;
|
||||
|
||||
// Read all button states once at the beginning
|
||||
bool pressButtonPressed = !digitalRead(_pinPress);
|
||||
bool upButtonPressed = !digitalRead(_pinUp);
|
||||
bool downButtonPressed = !digitalRead(_pinDown);
|
||||
|
||||
// Handle initial button press detection - only if not already detected
|
||||
if (this->action == UPDOWN_ACTION_PRESSED && pressButtonPressed && !pressDetected) {
|
||||
pressDetected = true;
|
||||
pressStartTime = now;
|
||||
} else if (this->action == UPDOWN_ACTION_UP && upButtonPressed && !upDetected) {
|
||||
upDetected = true;
|
||||
upStartTime = now;
|
||||
} else if (this->action == UPDOWN_ACTION_DOWN && downButtonPressed && !downDetected) {
|
||||
downDetected = true;
|
||||
downStartTime = now;
|
||||
}
|
||||
|
||||
// Handle long press detection for press button
|
||||
if (pressDetected && pressStartTime > 0) {
|
||||
uint32_t pressDuration = now - pressStartTime;
|
||||
|
||||
if (!pressButtonPressed) {
|
||||
// Button released
|
||||
if (pressDuration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
|
||||
lastPressKeyTime = now;
|
||||
e.inputEvent = this->_eventPressed;
|
||||
}
|
||||
// Reset state
|
||||
pressDetected = false;
|
||||
pressStartTime = 0;
|
||||
lastPressLongEventTime = 0;
|
||||
} else if (pressDuration >= LONG_PRESS_DURATION && lastPressLongEventTime == 0) {
|
||||
// First long press event only - avoid repeated events causing lag
|
||||
e.inputEvent = this->_eventPressedLong;
|
||||
lastPressLongEventTime = now;
|
||||
}
|
||||
} else if (this->action == UPDOWN_ACTION_UP) {
|
||||
if (now - lastUpKeyTime >= updownDebounceMs) {
|
||||
lastUpKeyTime = now;
|
||||
LOG_DEBUG("GPIO event Up");
|
||||
e.inputEvent = this->_eventUp;
|
||||
}
|
||||
|
||||
// Handle long press detection for up button
|
||||
if (upDetected && upStartTime > 0) {
|
||||
uint32_t upDuration = now - upStartTime;
|
||||
|
||||
if (!upButtonPressed) {
|
||||
// Button released
|
||||
if (upDuration < LONG_PRESS_DURATION && now - lastUpKeyTime >= updownDebounceMs) {
|
||||
lastUpKeyTime = now;
|
||||
e.inputEvent = this->_eventUp;
|
||||
}
|
||||
// Reset state
|
||||
upDetected = false;
|
||||
upStartTime = 0;
|
||||
lastUpLongEventTime = 0;
|
||||
} else if (upDuration >= LONG_PRESS_DURATION) {
|
||||
// Auto-repeat long press events
|
||||
if (lastUpLongEventTime == 0 || (now - lastUpLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
|
||||
e.inputEvent = this->_eventUpLong;
|
||||
lastUpLongEventTime = now;
|
||||
}
|
||||
}
|
||||
} else if (this->action == UPDOWN_ACTION_DOWN) {
|
||||
if (now - lastDownKeyTime >= updownDebounceMs) {
|
||||
lastDownKeyTime = now;
|
||||
LOG_DEBUG("GPIO event Down");
|
||||
e.inputEvent = this->_eventDown;
|
||||
}
|
||||
|
||||
// Handle long press detection for down button
|
||||
if (downDetected && downStartTime > 0) {
|
||||
uint32_t downDuration = now - downStartTime;
|
||||
|
||||
if (!downButtonPressed) {
|
||||
// Button released
|
||||
if (downDuration < LONG_PRESS_DURATION && now - lastDownKeyTime >= updownDebounceMs) {
|
||||
lastDownKeyTime = now;
|
||||
e.inputEvent = this->_eventDown;
|
||||
}
|
||||
// Reset state
|
||||
downDetected = false;
|
||||
downStartTime = 0;
|
||||
lastDownLongEventTime = 0;
|
||||
} else if (downDuration >= LONG_PRESS_DURATION) {
|
||||
// Auto-repeat long press events
|
||||
if (lastDownLongEventTime == 0 || (now - lastDownLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
|
||||
e.inputEvent = this->_eventDownLong;
|
||||
lastDownLongEventTime = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +135,11 @@ int32_t UpDownInterruptBase::runOnce()
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
|
||||
this->action = UPDOWN_ACTION_NONE;
|
||||
return 100;
|
||||
if (!pressDetected && !upDetected && !downDetected) {
|
||||
this->action = UPDOWN_ACTION_NONE;
|
||||
}
|
||||
|
||||
return 20; // This will control how the input frequency
|
||||
}
|
||||
|
||||
void UpDownInterruptBase::intPressHandler()
|
||||
|
||||
@@ -8,7 +8,8 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, public concur
|
||||
public:
|
||||
explicit UpDownInterruptBase(const char *name);
|
||||
void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp,
|
||||
input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(),
|
||||
input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong,
|
||||
input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(),
|
||||
unsigned long updownDebounceMs = 50);
|
||||
void intPressHandler();
|
||||
void intDownHandler();
|
||||
@@ -17,16 +18,41 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, public concur
|
||||
int32_t runOnce() override;
|
||||
|
||||
protected:
|
||||
enum UpDownInterruptBaseActionType { UPDOWN_ACTION_NONE, UPDOWN_ACTION_PRESSED, UPDOWN_ACTION_UP, UPDOWN_ACTION_DOWN };
|
||||
enum UpDownInterruptBaseActionType {
|
||||
UPDOWN_ACTION_NONE,
|
||||
UPDOWN_ACTION_PRESSED,
|
||||
UPDOWN_ACTION_PRESSED_LONG,
|
||||
UPDOWN_ACTION_UP,
|
||||
UPDOWN_ACTION_UP_LONG,
|
||||
UPDOWN_ACTION_DOWN,
|
||||
UPDOWN_ACTION_DOWN_LONG
|
||||
};
|
||||
|
||||
volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE;
|
||||
|
||||
// Long press detection variables
|
||||
uint32_t pressStartTime = 0;
|
||||
uint32_t upStartTime = 0;
|
||||
uint32_t downStartTime = 0;
|
||||
bool pressDetected = false;
|
||||
bool upDetected = false;
|
||||
bool downDetected = false;
|
||||
uint32_t lastPressLongEventTime = 0;
|
||||
uint32_t lastUpLongEventTime = 0;
|
||||
uint32_t lastDownLongEventTime = 0;
|
||||
static const uint32_t LONG_PRESS_DURATION = 300;
|
||||
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300;
|
||||
|
||||
private:
|
||||
uint8_t _pinDown = 0;
|
||||
uint8_t _pinUp = 0;
|
||||
uint8_t _pinPress = 0;
|
||||
input_broker_event _eventDown = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventUp = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventPressed = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventUpLong = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventDownLong = INPUT_BROKER_NONE;
|
||||
const char *_originName;
|
||||
|
||||
unsigned long lastUpKeyTime = 0;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "UpDownInterruptImpl1.h"
|
||||
#include "InputBroker.h"
|
||||
extern bool osk_found;
|
||||
|
||||
UpDownInterruptImpl1 *upDownInterruptImpl1;
|
||||
|
||||
@@ -20,10 +21,15 @@ bool UpDownInterruptImpl1::init()
|
||||
input_broker_event eventDown = INPUT_BROKER_DOWN;
|
||||
input_broker_event eventUp = INPUT_BROKER_UP;
|
||||
input_broker_event eventPressed = INPUT_BROKER_SELECT;
|
||||
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
|
||||
input_broker_event eventUpLong = INPUT_BROKER_UP_LONG;
|
||||
input_broker_event eventDownLong = INPUT_BROKER_DOWN_LONG;
|
||||
|
||||
UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, UpDownInterruptImpl1::handleIntDown,
|
||||
UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed);
|
||||
UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, eventPressedLong, eventUpLong,
|
||||
eventDownLong, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp,
|
||||
UpDownInterruptImpl1::handleIntPressed);
|
||||
inputBroker->registerSource(this);
|
||||
osk_found = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
14
src/main.cpp
14
src/main.cpp
@@ -191,6 +191,8 @@ ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE;
|
||||
uint8_t kb_model;
|
||||
// global bool to record that a kb is present
|
||||
bool kb_found = false;
|
||||
// global bool to record that on-screen keyboard (OSK) is present
|
||||
bool osk_found = false;
|
||||
|
||||
// The I2C address of the RTC Module (if found)
|
||||
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
|
||||
@@ -1412,6 +1414,10 @@ void setup()
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
|
||||
osk_found = true;
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER
|
||||
// Start web server thread.
|
||||
webServerThread = new WebServerThread();
|
||||
@@ -1562,7 +1568,13 @@ void loop()
|
||||
#endif
|
||||
|
||||
service->loop();
|
||||
|
||||
#if defined(LGFX_SDL)
|
||||
if (screen) {
|
||||
auto dispdev = screen->getDisplayDevice();
|
||||
if (dispdev)
|
||||
static_cast<TFTDisplay *>(dispdev)->sdlLoop();
|
||||
}
|
||||
#endif
|
||||
long delayMsec = mainController.runOrDelay();
|
||||
|
||||
// We want to sleep as long as possible here - because it saves power
|
||||
|
||||
@@ -32,6 +32,7 @@ extern ScanI2C::DeviceAddress screen_found;
|
||||
extern ScanI2C::DeviceAddress cardkb_found;
|
||||
extern uint8_t kb_model;
|
||||
extern bool kb_found;
|
||||
extern bool osk_found;
|
||||
extern ScanI2C::DeviceAddress rtc_found;
|
||||
extern ScanI2C::DeviceAddress accelerometer_found;
|
||||
extern ScanI2C::FoundDevice rgb_found;
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
#include "mesh/NodeDB.h"
|
||||
#ifdef LR11X0_DIO_AS_RF_SWITCH
|
||||
#include "rfswitch.h"
|
||||
#elif ARCH_PORTDUINO
|
||||
#include "PortduinoGlue.h"
|
||||
#define rfswitch_dio_pins portduino_config.rfswitch_dio_pins
|
||||
#define rfswitch_table portduino_config.rfswitch_table
|
||||
#else
|
||||
static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
|
||||
static const Module::RfSwitchMode_t rfswitch_table[] = {
|
||||
@@ -14,10 +18,6 @@ static const Module::RfSwitchMode_t rfswitch_table[] = {
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "PortduinoGlue.h"
|
||||
#endif
|
||||
|
||||
// Particular boards might define a different max power based on what their hardware can do, default to max power output if not
|
||||
// specified (may be dangerous if using external PA and LR11x0 power config forgotten)
|
||||
#if ARCH_PORTDUINO
|
||||
@@ -117,17 +117,14 @@ template <typename T> bool LR11x0Interface<T>::init()
|
||||
#ifdef LR11X0_DIO_AS_RF_SWITCH
|
||||
bool dioAsRfSwitch = true;
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
bool dioAsRfSwitch = false;
|
||||
if (settingsMap[dio2_as_rf_switch]) {
|
||||
dioAsRfSwitch = true;
|
||||
}
|
||||
bool dioAsRfSwitch = portduino_config.has_rfswitch_table;
|
||||
#else
|
||||
bool dioAsRfSwitch = false;
|
||||
#endif
|
||||
|
||||
if (dioAsRfSwitch) {
|
||||
lora.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table);
|
||||
LOG_DEBUG("Set DIO RF switch", res);
|
||||
LOG_DEBUG("Set DIO RF switch");
|
||||
}
|
||||
|
||||
if (res == RADIOLIB_ERR_NONE) {
|
||||
|
||||
@@ -225,7 +225,11 @@ NodeDB::NodeDB()
|
||||
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
|
||||
myNodeInfo.device_id.size = 16;
|
||||
// Uncomment below to print the device id
|
||||
|
||||
#elif ARCH_PORTDUINO
|
||||
if (portduino_config.has_device_id) {
|
||||
memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16);
|
||||
myNodeInfo.device_id.size = 16;
|
||||
}
|
||||
#else
|
||||
// FIXME - implement for other platforms
|
||||
#endif
|
||||
|
||||
@@ -192,12 +192,6 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
|
||||
size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
{
|
||||
if (!available()) {
|
||||
return 0;
|
||||
}
|
||||
// In case we send a FromRadio packet
|
||||
memset(&fromRadioScratch, 0, sizeof(fromRadioScratch));
|
||||
|
||||
// Respond to heartbeat by sending queue status
|
||||
if (heartbeatReceived) {
|
||||
memset(&fromRadioScratch, 0, sizeof(fromRadioScratch));
|
||||
@@ -209,6 +203,12 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
return numbytes;
|
||||
}
|
||||
|
||||
if (!available()) {
|
||||
return 0;
|
||||
}
|
||||
// In case we send a FromRadio packet
|
||||
memset(&fromRadioScratch, 0, sizeof(fromRadioScratch));
|
||||
|
||||
// Advance states as needed
|
||||
switch (state) {
|
||||
case STATE_SEND_NOTHING:
|
||||
|
||||
@@ -523,8 +523,10 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
// is not in the local nodedb
|
||||
// First, only PKC encrypt packets we are originating
|
||||
if (isFromUs(p) &&
|
||||
// Don't use PKC with simulator
|
||||
radioType != SIM_RADIO &&
|
||||
#if ARCH_PORTDUINO
|
||||
// Sim radio via the cli flag skips PKC
|
||||
!portduino_config.force_simradio &&
|
||||
#endif
|
||||
// Don't use PKC with Ham mode
|
||||
!owner.is_licensed &&
|
||||
// Don't use PKC if it's not explicitly requested and a non-primary channel is requested
|
||||
|
||||
@@ -207,10 +207,10 @@ typedef enum _meshtastic_Config_DisplayConfig_OledType {
|
||||
meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306 = 1,
|
||||
/* Default / Autodetect */
|
||||
meshtastic_Config_DisplayConfig_OledType_OLED_SH1106 = 2,
|
||||
/* Can not be auto detected but set by proto. Used for 128x128 screens */
|
||||
meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 = 3,
|
||||
/* Can not be auto detected but set by proto. Used for 128x64 screens */
|
||||
meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64 = 4
|
||||
meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 = 3,
|
||||
/* Can not be auto detected but set by proto. Used for 128x128 screens */
|
||||
meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128 = 4
|
||||
} meshtastic_Config_DisplayConfig_OledType;
|
||||
|
||||
typedef enum _meshtastic_Config_DisplayConfig_DisplayMode {
|
||||
@@ -682,8 +682,8 @@ extern "C" {
|
||||
#define _meshtastic_Config_DisplayConfig_DisplayUnits_ARRAYSIZE ((meshtastic_Config_DisplayConfig_DisplayUnits)(meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL+1))
|
||||
|
||||
#define _meshtastic_Config_DisplayConfig_OledType_MIN meshtastic_Config_DisplayConfig_OledType_OLED_AUTO
|
||||
#define _meshtastic_Config_DisplayConfig_OledType_MAX meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64
|
||||
#define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64+1))
|
||||
#define _meshtastic_Config_DisplayConfig_OledType_MAX meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128
|
||||
#define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128+1))
|
||||
|
||||
#define _meshtastic_Config_DisplayConfig_DisplayMode_MIN meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT
|
||||
#define _meshtastic_Config_DisplayConfig_DisplayMode_MAX meshtastic_Config_DisplayConfig_DisplayMode_COLOR
|
||||
|
||||
@@ -99,7 +99,9 @@ typedef enum _meshtastic_TelemetrySensorType {
|
||||
/* Sensirion SFA30 Formaldehyde sensor */
|
||||
meshtastic_TelemetrySensorType_SFA30 = 42,
|
||||
/* SEN5X PM SENSORS */
|
||||
meshtastic_TelemetrySensorType_SEN5X = 43
|
||||
meshtastic_TelemetrySensorType_SEN5X = 43,
|
||||
/* TSL2561 light sensor */
|
||||
meshtastic_TelemetrySensorType_TSL2561 = 44
|
||||
} meshtastic_TelemetrySensorType;
|
||||
|
||||
/* Struct definitions */
|
||||
@@ -434,8 +436,8 @@ extern "C" {
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SEN5X
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SEN5X+1))
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_TSL2561
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_TSL2561+1))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -13,12 +13,16 @@
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/NotificationRenderer.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "graphics/images.h"
|
||||
#include "main.h" // for cardkb_found
|
||||
#include "mesh/generated/meshtastic/cannedmessages.pb.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/ExternalNotificationModule.h" // for buzzer control
|
||||
#if HAS_TRACKBALL
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "GPS.h"
|
||||
#endif
|
||||
@@ -38,6 +42,7 @@
|
||||
|
||||
extern ScanI2C::DeviceAddress cardkb_found;
|
||||
extern bool graphics::isMuted;
|
||||
extern bool osk_found;
|
||||
|
||||
static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto";
|
||||
static NodeNum lastDest = NODENUM_BROADCAST;
|
||||
@@ -151,10 +156,13 @@ int CannedMessageModule::splitConfiguredMessages()
|
||||
int tempCount = 0;
|
||||
// Insert at position 0 (top)
|
||||
tempMessages[tempCount++] = "[Select Destination]";
|
||||
|
||||
#if defined(USE_VIRTUAL_KEYBOARD)
|
||||
// Add a "Free Text" entry at the top if using a keyboard
|
||||
// Add a "Free Text" entry at the top if using a touch screen virtual keyboard
|
||||
tempMessages[tempCount++] = "[-- Free Text --]";
|
||||
#else
|
||||
if (osk_found && screen) {
|
||||
tempMessages[tempCount++] = "[-- Free Text --]";
|
||||
}
|
||||
#endif
|
||||
|
||||
// First message always starts at buffer start
|
||||
@@ -341,6 +349,8 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
|
||||
case CANNED_MESSAGE_RUN_STATE_FREETEXT:
|
||||
return handleFreeTextInput(event); // All allowed input for this state
|
||||
|
||||
// Virtual keyboard mode: Show virtual keyboard and handle input
|
||||
|
||||
// If sending, block all input except global/system (handled above)
|
||||
case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE:
|
||||
return 1;
|
||||
@@ -627,6 +637,56 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
|
||||
notifyObservers(&e);
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
if (strcmp(current, "[-- Free Text --]") == 0) {
|
||||
if (osk_found && screen) {
|
||||
char headerBuffer[64];
|
||||
if (this->dest == NODENUM_BROADCAST) {
|
||||
snprintf(headerBuffer, sizeof(headerBuffer), "To: Broadcast@%s", channels.getName(this->channel));
|
||||
} else {
|
||||
snprintf(headerBuffer, sizeof(headerBuffer), "To: %s", getNodeName(this->dest));
|
||||
}
|
||||
screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) {
|
||||
if (!text.empty()) {
|
||||
this->freetext = text.c_str();
|
||||
this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
||||
runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
|
||||
currentMessageIndex = -1;
|
||||
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->notifyObservers(&e);
|
||||
screen->forceDisplay();
|
||||
|
||||
setIntervalFromNow(500);
|
||||
return;
|
||||
} else {
|
||||
// Don't delete virtual keyboard immediately - it might still be executing
|
||||
// Instead, just clear the callback and reset banner to stop input processing
|
||||
graphics::NotificationRenderer::textInputCallback = nullptr;
|
||||
graphics::NotificationRenderer::resetBanner();
|
||||
|
||||
// Return to inactive state
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
this->currentMessageIndex = -1;
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
|
||||
// Force display update to show normal screen
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->notifyObservers(&e);
|
||||
screen->forceDisplay();
|
||||
|
||||
// Schedule cleanup for next loop iteration to ensure safe deletion
|
||||
setIntervalFromNow(50);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Normal canned message selection
|
||||
@@ -943,12 +1003,54 @@ int32_t CannedMessageModule::runOnce()
|
||||
|
||||
// Normal module disable/idle handling
|
||||
if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) {
|
||||
// Clean up virtual keyboard if needed when going inactive
|
||||
if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) {
|
||||
LOG_INFO("Performing delayed virtual keyboard cleanup");
|
||||
delete graphics::NotificationRenderer::virtualKeyboard;
|
||||
graphics::NotificationRenderer::virtualKeyboard = nullptr;
|
||||
}
|
||||
|
||||
temporaryMessage = "";
|
||||
return INT32_MAX;
|
||||
}
|
||||
|
||||
// Handle delayed virtual keyboard message sending
|
||||
if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
// Virtual keyboard message sending case - text was not empty
|
||||
if (this->freetext.length() > 0) {
|
||||
LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str());
|
||||
sendText(this->dest, this->channel, this->freetext.c_str(), true);
|
||||
|
||||
// Clean up virtual keyboard after sending
|
||||
if (graphics::NotificationRenderer::virtualKeyboard) {
|
||||
LOG_INFO("Cleaning up virtual keyboard after message send");
|
||||
delete graphics::NotificationRenderer::virtualKeyboard;
|
||||
graphics::NotificationRenderer::virtualKeyboard = nullptr;
|
||||
graphics::NotificationRenderer::textInputCallback = nullptr;
|
||||
graphics::NotificationRenderer::resetBanner();
|
||||
}
|
||||
|
||||
// Clear payload to indicate virtual keyboard processing is complete
|
||||
// But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds
|
||||
this->payload = 0;
|
||||
} else {
|
||||
// Empty message, just go inactive
|
||||
LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state");
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
}
|
||||
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->currentMessageIndex = -1;
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->notifyObservers(&e);
|
||||
return 2000;
|
||||
}
|
||||
|
||||
UIFrameEvent e;
|
||||
if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) ||
|
||||
if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 &&
|
||||
this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) ||
|
||||
(this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) ||
|
||||
(this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) {
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
@@ -958,6 +1060,18 @@ int32_t CannedMessageModule::runOnce()
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
// Handle SENDING_ACTIVE state transition after virtual keyboard message
|
||||
else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) {
|
||||
// This happens after virtual keyboard message sending is complete
|
||||
LOG_INFO("Virtual keyboard message sending completed, returning to inactive state");
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
temporaryMessage = "";
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->currentMessageIndex = -1;
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->notifyObservers(&e);
|
||||
} else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) &&
|
||||
!Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) {
|
||||
// Reset module on inactivity
|
||||
@@ -966,9 +1080,23 @@ int32_t CannedMessageModule::runOnce()
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
|
||||
// Clean up virtual keyboard if it exists during timeout
|
||||
if (graphics::NotificationRenderer::virtualKeyboard) {
|
||||
LOG_INFO("Cleaning up virtual keyboard due to module timeout");
|
||||
delete graphics::NotificationRenderer::virtualKeyboard;
|
||||
graphics::NotificationRenderer::virtualKeyboard = nullptr;
|
||||
graphics::NotificationRenderer::textInputCallback = nullptr;
|
||||
graphics::NotificationRenderer::resetBanner();
|
||||
}
|
||||
|
||||
this->notifyObservers(&e);
|
||||
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) {
|
||||
if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
if (this->payload == 0) {
|
||||
// [Exit] button pressed - return to inactive state
|
||||
LOG_INFO("Processing [Exit] action - returning to inactive state");
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
} else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
if (this->freetext.length() > 0) {
|
||||
sendText(this->dest, this->channel, this->freetext.c_str(), true);
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
|
||||
|
||||
@@ -279,6 +279,8 @@ struct PubSubConfig {
|
||||
|
||||
// Defaults
|
||||
static constexpr uint16_t defaultPort = 1883;
|
||||
static constexpr uint16_t defaultPortTls = 8883;
|
||||
|
||||
uint16_t serverPort = defaultPort;
|
||||
String serverAddr = default_mqtt_address;
|
||||
const char *mqttUsername = default_mqtt_username;
|
||||
@@ -641,7 +643,7 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC
|
||||
}
|
||||
|
||||
const bool defaultServer = isDefaultServer(parsed.serverAddr);
|
||||
if (defaultServer && parsed.serverPort != PubSubConfig::defaultPort) {
|
||||
if (defaultServer && !IS_ONE_OF(parsed.serverPort, PubSubConfig::defaultPort, PubSubConfig::defaultPortTls)) {
|
||||
const char *warning = "Invalid MQTT config: default server address must not have a port specified";
|
||||
LOG_ERROR(warning);
|
||||
#if !IS_RUNNING_TESTS
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "linux/gpio/LinuxGPIOPin.h"
|
||||
#include "meshUtils.h"
|
||||
#include "yaml-cpp/yaml.h"
|
||||
#include <ErriezCRC32.h>
|
||||
#include <Utility.h>
|
||||
#include <assert.h>
|
||||
#include <bluetooth/bluetooth.h>
|
||||
@@ -29,11 +30,11 @@
|
||||
|
||||
std::map<configNames, int> settingsMap;
|
||||
std::map<configNames, std::string> settingsStrings;
|
||||
portduino_config_struct portduino_config;
|
||||
std::ofstream traceFile;
|
||||
Ch341Hal *ch341Hal = nullptr;
|
||||
char *configPath = nullptr;
|
||||
char *optionMac = nullptr;
|
||||
bool forceSimulated = false;
|
||||
bool verboseEnabled = false;
|
||||
|
||||
const char *argp_program_version = optstr(APP_VERSION);
|
||||
@@ -66,7 +67,7 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state)
|
||||
configPath = arg;
|
||||
break;
|
||||
case 's':
|
||||
forceSimulated = true;
|
||||
portduino_config.force_simradio = true;
|
||||
break;
|
||||
case 'h':
|
||||
optionMac = arg;
|
||||
@@ -189,7 +190,7 @@ void portduinoSetup()
|
||||
|
||||
YAML::Node yamlConfig;
|
||||
|
||||
if (forceSimulated == true) {
|
||||
if (portduino_config.force_simradio == true) {
|
||||
settingsMap[use_simradio] = true;
|
||||
} else if (configPath != nullptr) {
|
||||
if (loadConfig(configPath)) {
|
||||
@@ -253,16 +254,95 @@ void portduinoSetup()
|
||||
std::cout << "autoconf: Could not locate CH341 device" << std::endl;
|
||||
}
|
||||
// Try Pi HAT+
|
||||
std::cout << "autoconf: Looking for Pi HAT+..." << std::endl;
|
||||
if (access("/proc/device-tree/hat/product", R_OK) == 0) {
|
||||
std::ifstream hatProductFile("/proc/device-tree/hat/product");
|
||||
if (hatProductFile.is_open()) {
|
||||
hatProductFile.read(autoconf_product, 95);
|
||||
hatProductFile.close();
|
||||
if (strlen(autoconf_product) < 6) {
|
||||
std::cout << "autoconf: Looking for Pi HAT+..." << std::endl;
|
||||
if (access("/proc/device-tree/hat/product", R_OK) == 0) {
|
||||
std::ifstream hatProductFile("/proc/device-tree/hat/product");
|
||||
if (hatProductFile.is_open()) {
|
||||
hatProductFile.read(autoconf_product, 95);
|
||||
hatProductFile.close();
|
||||
}
|
||||
std::cout << "autoconf: Found Pi HAT+ " << autoconf_product << " at /proc/device-tree/hat/product" << std::endl;
|
||||
} else {
|
||||
std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat/product" << std::endl;
|
||||
}
|
||||
}
|
||||
// attempt to load autoconf data from an EEPROM on 0x50
|
||||
// RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8
|
||||
// <model string>:mac address :<16 random unique bytes in hexidecimal> : crc32
|
||||
// crc32 is calculated on the eeprom string up to but not including the final colon
|
||||
if (strlen(autoconf_product) < 6) {
|
||||
try {
|
||||
char *mac_start = nullptr;
|
||||
char *devID_start = nullptr;
|
||||
char *crc32_start = nullptr;
|
||||
Wire.begin();
|
||||
Wire.beginTransmission(0x50);
|
||||
Wire.write(0x0);
|
||||
Wire.write(0x0);
|
||||
Wire.endTransmission();
|
||||
Wire.requestFrom((uint8_t)0x50, (uint8_t)75);
|
||||
uint8_t i = 0;
|
||||
delay(100);
|
||||
std::string autoconf_raw;
|
||||
while (Wire.available() && i < sizeof(autoconf_product)) {
|
||||
autoconf_product[i] = Wire.read();
|
||||
if (autoconf_product[i] == 0xff) {
|
||||
autoconf_product[i] = 0x0;
|
||||
break;
|
||||
}
|
||||
autoconf_raw += autoconf_product[i];
|
||||
if (autoconf_product[i] == ':') {
|
||||
autoconf_product[i] = 0x0;
|
||||
if (mac_start == nullptr) {
|
||||
mac_start = autoconf_product + i + 1;
|
||||
} else if (devID_start == nullptr) {
|
||||
devID_start = autoconf_product + i + 1;
|
||||
} else if (crc32_start == nullptr) {
|
||||
crc32_start = autoconf_product + i + 1;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (crc32_start != nullptr && strlen(crc32_start) == 8) {
|
||||
std::string crc32_str(crc32_start);
|
||||
uint32_t crc32_value = 0;
|
||||
|
||||
// convert crc32 ascii to raw uint32
|
||||
for (int j = 0; j < 4; j++) {
|
||||
crc32_value += std::stoi(crc32_str.substr(j * 2, 2), nullptr, 16) << (3 - j) * 8;
|
||||
}
|
||||
std::cout << "autoconf: Found eeprom crc " << crc32_start << std::endl;
|
||||
|
||||
// set the autoconf string to blank and short circuit
|
||||
if (crc32_value != crc32Buffer(autoconf_raw.c_str(), i - 9)) {
|
||||
std::cout << "autoconf: crc32 mismatch, dropping " << std::endl;
|
||||
autoconf_product[0] = 0x0;
|
||||
} else {
|
||||
std::cout << "autoconf: Found eeprom data " << autoconf_raw << std::endl;
|
||||
if (mac_start != nullptr) {
|
||||
std::cout << "autoconf: Found mac data " << mac_start << std::endl;
|
||||
if (strlen(mac_start) == 12)
|
||||
settingsStrings[mac_address] = std::string(mac_start);
|
||||
}
|
||||
if (devID_start != nullptr) {
|
||||
std::cout << "autoconf: Found deviceid data " << devID_start << std::endl;
|
||||
if (strlen(devID_start) == 32) {
|
||||
std::string devID_str(devID_start);
|
||||
for (int j = 0; j < 16; j++) {
|
||||
portduino_config.device_id[j] = std::stoi(devID_str.substr(j * 2, 2), nullptr, 16);
|
||||
}
|
||||
portduino_config.has_device_id = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::cout << "autoconf: crc32 missing " << std::endl;
|
||||
autoconf_product[0] = 0x0;
|
||||
}
|
||||
} catch (...) {
|
||||
std::cout << "autoconf: Could not locate EEPROM" << std::endl;
|
||||
}
|
||||
std::cout << "autoconf: Found Pi HAT+ " << autoconf_product << " at /proc/device-tree/hat/product" << std::endl;
|
||||
} else {
|
||||
std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat/product" << std::endl;
|
||||
}
|
||||
// Load the config file based on the product string
|
||||
if (strlen(autoconf_product) > 0) {
|
||||
@@ -553,6 +633,48 @@ bool loadConfig(const char *configPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]) {
|
||||
portduino_config.has_rfswitch_table = true;
|
||||
portduino_config.rfswitch_table[0].mode = LR11x0::MODE_STBY;
|
||||
portduino_config.rfswitch_table[1].mode = LR11x0::MODE_RX;
|
||||
portduino_config.rfswitch_table[2].mode = LR11x0::MODE_TX;
|
||||
portduino_config.rfswitch_table[3].mode = LR11x0::MODE_TX_HP;
|
||||
portduino_config.rfswitch_table[4].mode = LR11x0::MODE_TX_HF;
|
||||
portduino_config.rfswitch_table[5].mode = LR11x0::MODE_GNSS;
|
||||
portduino_config.rfswitch_table[6].mode = LR11x0::MODE_WIFI;
|
||||
portduino_config.rfswitch_table[7] = END_OF_MODE_TABLE;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
|
||||
// set up the pin array first
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as<std::string>("") == "DIO5")
|
||||
portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO5;
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as<std::string>("") == "DIO6")
|
||||
portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO6;
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as<std::string>("") == "DIO7")
|
||||
portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO7;
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as<std::string>("") == "DIO8")
|
||||
portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO8;
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as<std::string>("") == "DIO10")
|
||||
portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO10;
|
||||
|
||||
// now fill in the table
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_STBY"][i].as<std::string>("") == "HIGH")
|
||||
portduino_config.rfswitch_table[0].values[i] = HIGH;
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_RX"][i].as<std::string>("") == "HIGH")
|
||||
portduino_config.rfswitch_table[1].values[i] = HIGH;
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX"][i].as<std::string>("") == "HIGH")
|
||||
portduino_config.rfswitch_table[2].values[i] = HIGH;
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HP"][i].as<std::string>("") == "HIGH")
|
||||
portduino_config.rfswitch_table[3].values[i] = HIGH;
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HF"][i].as<std::string>("") == "HIGH")
|
||||
portduino_config.rfswitch_table[4].values[i] = HIGH;
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_GNSS"][i].as<std::string>("") == "HIGH")
|
||||
portduino_config.rfswitch_table[5].values[i] = HIGH;
|
||||
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_WIFI"][i].as<std::string>("") == "HIGH")
|
||||
portduino_config.rfswitch_table[6].values[i] = HIGH;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (yamlConfig["GPIO"]) {
|
||||
settingsMap[userButtonPin] = yamlConfig["GPIO"]["User"].as<int>(RADIOLIB_NC);
|
||||
|
||||
@@ -3,16 +3,21 @@
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "LR11x0Interface.h"
|
||||
#include "Module.h"
|
||||
#include "platform/portduino/USBHal.h"
|
||||
|
||||
// Product strings for auto-configuration
|
||||
// {"PRODUCT_STRING", "CONFIG.YAML"}
|
||||
// YAML paths are relative to `meshtastic/available.d`
|
||||
inline const std::unordered_map<std::string, std::string> configProducts = {{"MESHTOAD", "lora-usb-meshtoad-e22.yaml"},
|
||||
{"MESHSTICK", "lora-meshstick-1262.yaml"},
|
||||
{"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"},
|
||||
{"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"},
|
||||
{"POWERPI", "lora-MeshAdv-900M30S.yaml"}};
|
||||
inline const std::unordered_map<std::string, std::string> configProducts = {
|
||||
{"MESHTOAD", "lora-usb-meshtoad-e22.yaml"},
|
||||
{"MESHSTICK", "lora-meshstick-1262.yaml"},
|
||||
{"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"},
|
||||
{"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"},
|
||||
{"POWERPI", "lora-MeshAdv-900M30S.yaml"},
|
||||
{"RAK6421-13300-S1", "lora-RAK6421-13300-slot1.yaml"},
|
||||
{"RAK6421-13300-S2", "lora-RAK6421-13300-slot2.yaml"}};
|
||||
|
||||
enum configNames {
|
||||
default_gpiochip,
|
||||
@@ -126,4 +131,13 @@ bool loadConfig(const char *configPath);
|
||||
static bool ends_with(std::string_view str, std::string_view suffix);
|
||||
void getMacAddr(uint8_t *dmac);
|
||||
bool MAC_from_string(std::string mac_str, uint8_t *dmac);
|
||||
std::string exec(const char *cmd);
|
||||
std::string exec(const char *cmd);
|
||||
|
||||
extern struct portduino_config_struct {
|
||||
bool has_rfswitch_table = false;
|
||||
uint32_t rfswitch_dio_pins[5] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
|
||||
Module::RfSwitchMode_t rfswitch_table[8];
|
||||
bool force_simradio = false;
|
||||
bool has_device_id = false;
|
||||
uint8_t device_id[16] = {0};
|
||||
} portduino_config;
|
||||
@@ -25,7 +25,6 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio
|
||||
-D USE_X11=1
|
||||
-D HAS_TFT=1
|
||||
-D HAS_SCREEN=1
|
||||
|
||||
-D LV_CACHE_DEF_SIZE=6291456
|
||||
-D LV_BUILD_TEST=0
|
||||
-D LV_USE_LIBINPUT=1
|
||||
@@ -41,6 +40,25 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio
|
||||
build_src_filter =
|
||||
${native_base.build_src_filter}
|
||||
|
||||
[env:native-sdl]
|
||||
extends = native_base
|
||||
build_type = release
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
${networking_base.lib_deps}
|
||||
${radiolib_base.lib_deps}
|
||||
${environmental_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
|
||||
rweather/Crypto@0.4.0
|
||||
# renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main
|
||||
https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip
|
||||
# renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library
|
||||
adafruit/Adafruit seesaw Library@1.7.9
|
||||
https://github.com/jp-bennett/LovyanGFX/archive/7458f84a126c1f8fdc7b038074f71be903f6e4c0.zip
|
||||
build_flags = ${native_base.build_flags}
|
||||
!pkg-config --cflags --libs sdl2 --silence-errors || :
|
||||
-D LGFX_SDL=1
|
||||
|
||||
[env:native-fb]
|
||||
extends = native_base
|
||||
build_type = release
|
||||
|
||||
19
variants/nrf52840/meshtiny/platformio.ini
Normal file
19
variants/nrf52840/meshtiny/platformio.ini
Normal file
@@ -0,0 +1,19 @@
|
||||
; MeshTiny - Custom device based on GAT562 with encoder and buzzer support
|
||||
[env:meshtiny]
|
||||
extends = nrf52840_base
|
||||
board = meshtiny
|
||||
board_level = extra
|
||||
build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/meshtiny -D MESHTINY
|
||||
-DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely.
|
||||
-DRADIOLIB_EXCLUDE_SX128X=1
|
||||
-DRADIOLIB_EXCLUDE_SX127X=1
|
||||
-DRADIOLIB_EXCLUDE_LR11X0=1
|
||||
-D INPUTDRIVER_ENCODER_TYPE=2
|
||||
-D INPUTDRIVER_ENCODER_UP=4
|
||||
-D INPUTDRIVER_ENCODER_DOWN=26
|
||||
-D INPUTDRIVER_ENCODER_BTN=28
|
||||
-D USE_PIN_BUZZER=PIN_BUZZER
|
||||
-D MESHTASTIC_EXCLUDE_GPS=1
|
||||
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshtiny>
|
||||
lib_deps =
|
||||
${nrf52840_base.lib_deps}
|
||||
54
variants/nrf52840/meshtiny/variant.cpp
Normal file
54
variants/nrf52840/meshtiny/variant.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
|
||||
Copyright (c) 2016 Sandeep Mistry All right reserved.
|
||||
Copyright (c) 2018, Adafruit Industries (adafruit.com)
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library 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 Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "variant.h"
|
||||
#include "nrf.h"
|
||||
#include "wiring_constants.h"
|
||||
#include "wiring_digital.h"
|
||||
|
||||
const uint32_t g_ADigitalPinMap[] = {
|
||||
// P0
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
|
||||
|
||||
// P1
|
||||
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
|
||||
|
||||
void initVariant()
|
||||
{
|
||||
// LED1 & LED2
|
||||
pinMode(PIN_LED1, OUTPUT);
|
||||
ledOff(PIN_LED1);
|
||||
|
||||
pinMode(PIN_LED2, OUTPUT);
|
||||
ledOff(PIN_LED2);
|
||||
|
||||
// 3V3 Power Rail
|
||||
pinMode(PIN_3V3_EN, OUTPUT);
|
||||
digitalWrite(PIN_3V3_EN, HIGH);
|
||||
|
||||
// Initialize Encoder pins
|
||||
pinMode(INPUTDRIVER_ENCODER_UP, INPUT_PULLUP);
|
||||
pinMode(INPUTDRIVER_ENCODER_DOWN, INPUT_PULLUP);
|
||||
pinMode(INPUTDRIVER_ENCODER_BTN, INPUT_PULLUP);
|
||||
|
||||
// Initialize Buzzer pin
|
||||
pinMode(PIN_BUZZER, OUTPUT);
|
||||
digitalWrite(PIN_BUZZER, LOW);
|
||||
}
|
||||
199
variants/nrf52840/meshtiny/variant.h
Normal file
199
variants/nrf52840/meshtiny/variant.h
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
|
||||
Copyright (c) 2016 Sandeep Mistry All right reserved.
|
||||
Copyright (c) 2018, Adafruit Industries (adafruit.com)
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
This library 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 Lesser General Public License for more details.
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _VARIANT_MESHTINY_
|
||||
#define _VARIANT_MESHTINY_
|
||||
|
||||
#define MESHTINY
|
||||
|
||||
// #define RAK4630
|
||||
|
||||
/** Master clock frequency */
|
||||
#define VARIANT_MCK (64000000ul)
|
||||
|
||||
#define USE_LFXO // Board uses 32khz crystal for LF
|
||||
// define USE_LFRC // Board uses RC for LF
|
||||
|
||||
/*----------------------------------------------------------------------------
|
||||
* Headers
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "WVariant.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif // __cplusplus
|
||||
|
||||
// Number of pins defined in PinDescription array
|
||||
#define PINS_COUNT (48)
|
||||
#define NUM_DIGITAL_PINS (48)
|
||||
#define NUM_ANALOG_INPUTS (6)
|
||||
#define NUM_ANALOG_OUTPUTS (0)
|
||||
|
||||
// LEDs
|
||||
#define PIN_LED1 (35)
|
||||
#define PIN_LED2 (36)
|
||||
|
||||
#define LED_BUILTIN PIN_LED1
|
||||
#define LED_CONN PIN_LED2
|
||||
|
||||
#define LED_GREEN PIN_LED1
|
||||
#define LED_BLUE PIN_LED2
|
||||
|
||||
#define LED_STATE_ON 1 // State when LED is litted
|
||||
|
||||
/*
|
||||
* Encoder
|
||||
*/
|
||||
#define INPUTDRIVER_ENCODER_TYPE 2
|
||||
#define INPUTDRIVER_ENCODER_UP 26
|
||||
#define INPUTDRIVER_ENCODER_DOWN 4
|
||||
#define INPUTDRIVER_ENCODER_BTN 28
|
||||
|
||||
#define CANNED_MESSAGE_MODULE_ENABLE 1
|
||||
|
||||
/*
|
||||
* Buzzer - PWM
|
||||
*/
|
||||
#define PIN_BUZZER 30
|
||||
|
||||
/*
|
||||
* Buttons
|
||||
*/
|
||||
|
||||
#define PIN_BUTTON1 9
|
||||
#define BUTTON_NEED_PULLUP
|
||||
#define PIN_BUTTON2 12
|
||||
#define PIN_BUTTON3 24
|
||||
#define PIN_BUTTON4 25
|
||||
|
||||
/*
|
||||
* Analog pins
|
||||
*/
|
||||
#define PIN_A0 (5)
|
||||
#define PIN_A1 (31)
|
||||
#define PIN_A2 (28)
|
||||
#define PIN_A3 (29)
|
||||
#define PIN_A4 (30)
|
||||
#define PIN_A5 (31)
|
||||
#define PIN_A6 (0xff)
|
||||
#define PIN_A7 (0xff)
|
||||
|
||||
static const uint8_t A0 = PIN_A0;
|
||||
static const uint8_t A1 = PIN_A1;
|
||||
static const uint8_t A2 = PIN_A2;
|
||||
static const uint8_t A3 = PIN_A3;
|
||||
static const uint8_t A4 = PIN_A4;
|
||||
static const uint8_t A5 = PIN_A5;
|
||||
static const uint8_t A6 = PIN_A6;
|
||||
static const uint8_t A7 = PIN_A7;
|
||||
#define ADC_RESOLUTION 14
|
||||
|
||||
// Other pins
|
||||
#define PIN_AREF (2)
|
||||
#define PIN_NFC1 (9)
|
||||
#define PIN_NFC2 (10)
|
||||
|
||||
static const uint8_t AREF = PIN_AREF;
|
||||
|
||||
/*
|
||||
* Serial interfaces
|
||||
*/
|
||||
#define PIN_SERIAL1_RX (15)
|
||||
#define PIN_SERIAL1_TX (16)
|
||||
|
||||
// Connected to Jlink CDC
|
||||
#define PIN_SERIAL2_RX (8)
|
||||
#define PIN_SERIAL2_TX (6)
|
||||
|
||||
/*
|
||||
* SPI Interfaces
|
||||
*/
|
||||
#define SPI_INTERFACES_COUNT 2
|
||||
|
||||
#define PIN_SPI_MISO (45)
|
||||
#define PIN_SPI_MOSI (44)
|
||||
#define PIN_SPI_SCK (43)
|
||||
|
||||
#define PIN_SPI1_MISO (29) // (0 + 29)
|
||||
#define PIN_SPI1_MOSI (30) // (0 + 30)
|
||||
#define PIN_SPI1_SCK (3) // (0 + 3)
|
||||
|
||||
static const uint8_t SS = 42;
|
||||
static const uint8_t MOSI = PIN_SPI_MOSI;
|
||||
static const uint8_t MISO = PIN_SPI_MISO;
|
||||
static const uint8_t SCK = PIN_SPI_SCK;
|
||||
|
||||
#define HAS_SCREEN 1
|
||||
#define USE_SSD1306
|
||||
|
||||
/*
|
||||
* Wire Interfaces
|
||||
*/
|
||||
#define WIRE_INTERFACES_COUNT 1
|
||||
|
||||
#define PIN_WIRE_SDA (13)
|
||||
#define PIN_WIRE_SCL (14)
|
||||
|
||||
// QSPI Pins
|
||||
#define PIN_QSPI_SCK 3
|
||||
#define PIN_QSPI_CS 22 // Changed from 26 to avoid conflict with encoder
|
||||
#define PIN_QSPI_IO0 27 // Changed from 30 to avoid conflict with buzzer
|
||||
#define PIN_QSPI_IO1 29
|
||||
#define PIN_QSPI_IO2 21 // Changed from 28 to avoid conflict with encoder button
|
||||
#define PIN_QSPI_IO3 2
|
||||
|
||||
// On-board QSPI Flash
|
||||
#define EXTERNAL_FLASH_DEVICES IS25LP080D
|
||||
#define EXTERNAL_FLASH_USE_QSPI
|
||||
|
||||
#define USE_SX1262
|
||||
#define SX126X_CS (42)
|
||||
#define SX126X_DIO1 (47)
|
||||
#define SX126X_BUSY (46)
|
||||
#define SX126X_RESET (38)
|
||||
#define SX126X_POWER_EN (37)
|
||||
// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3
|
||||
#define SX126X_DIO2_AS_RF_SWITCH
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||
|
||||
// Testing USB detection
|
||||
#define NRF_APM
|
||||
|
||||
#define PIN_3V3_EN (34)
|
||||
|
||||
// Battery
|
||||
// The battery sense is hooked to pin A0 (5)
|
||||
#define BATTERY_PIN PIN_A0
|
||||
// and has 12 bit resolution
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#define BATTERY_SENSE_RESOLUTION 4096.0
|
||||
#undef AREF_VOLTAGE
|
||||
#define AREF_VOLTAGE 3.0
|
||||
#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
|
||||
#define ADC_MULTIPLIER 1.73
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/*----------------------------------------------------------------------------
|
||||
* Arduino objects - C++ only
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user