mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-14 23:41:29 +10:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90e2083d2c | ||
|
|
af1d4e08ff | ||
|
|
d772cd4431 | ||
|
|
b7b8071056 | ||
|
|
4a3d28f06b | ||
|
|
0f4210d2e8 | ||
|
|
dc6e109329 | ||
|
|
10141b2562 | ||
|
|
770085ddf7 | ||
|
|
833f950edc |
@@ -20,7 +20,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
|
|||||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
|
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
|
||||||
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
|
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
|
||||||
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev && \
|
libusb-1.0-0-dev libssl-dev pkg-config && \
|
||||||
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
||||||
pip install --no-cache-dir -U \
|
pip install --no-cache-dir -U \
|
||||||
platformio==6.1.16 \
|
platformio==6.1.16 \
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ runs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
- name: PlatformIO ${{ inputs.arch }} download cache
|
- name: PlatformIO ${{ inputs.arch }} download cache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio/.cache
|
path: ~/.platformio/.cache
|
||||||
key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }}
|
key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }}
|
||||||
@@ -100,7 +100,7 @@ runs:
|
|||||||
id: version
|
id: version
|
||||||
|
|
||||||
- name: Store binaries as an artifact
|
- name: Store binaries as an artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}
|
name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|||||||
@@ -1,314 +0,0 @@
|
|||||||
# Meshtastic Firmware - Copilot Instructions
|
|
||||||
|
|
||||||
This document provides context and guidelines for AI assistants working with the Meshtastic firmware codebase.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Meshtastic is an open-source LoRa mesh networking project for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware enables text messaging, location sharing, and telemetry over a decentralized mesh network.
|
|
||||||
|
|
||||||
### Supported Hardware Platforms
|
|
||||||
|
|
||||||
- **ESP32** (ESP32, ESP32-S3, ESP32-C3) - Most common platform
|
|
||||||
- **nRF52** (nRF52840, nRF52833) - Low power Nordic chips
|
|
||||||
- **RP2040/RP2350** - Raspberry Pi Pico variants
|
|
||||||
- **STM32WL** - STM32 with integrated LoRa
|
|
||||||
- **Linux/Portduino** - Native Linux builds (Raspberry Pi, etc.)
|
|
||||||
|
|
||||||
### Supported Radio Chips
|
|
||||||
|
|
||||||
- **SX1262/SX1268** - Sub-GHz LoRa (868/915 MHz regions)
|
|
||||||
- **SX1280** - 2.4 GHz LoRa
|
|
||||||
- **LR1110/LR1120/LR1121** - Wideband radios (sub-GHz and 2.4 GHz capable, but not simultaneously)
|
|
||||||
- **RF95** - Legacy RFM95 modules
|
|
||||||
- **LLCC68** - Low-cost LoRa
|
|
||||||
|
|
||||||
### MQTT Integration
|
|
||||||
|
|
||||||
MQTT provides a bridge between Meshtastic mesh networks and the internet, enabling nodes with network connectivity to share messages with remote meshes or external services.
|
|
||||||
|
|
||||||
#### Key Components
|
|
||||||
|
|
||||||
- **`src/mqtt/MQTT.cpp`** - Main MQTT client singleton, handles connection and message routing
|
|
||||||
- **`src/mqtt/ServiceEnvelope.cpp`** - Protobuf wrapper for mesh packets sent over MQTT
|
|
||||||
- **`moduleConfig.mqtt`** - MQTT module configuration
|
|
||||||
|
|
||||||
#### MQTT Topic Structure
|
|
||||||
|
|
||||||
Messages are published/subscribed using a hierarchical topic format:
|
|
||||||
|
|
||||||
```
|
|
||||||
{root}/{channel_id}/{gateway_id}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `root` - Configurable prefix (default: `msh`)
|
|
||||||
- `channel_id` - Channel name/identifier
|
|
||||||
- `gateway_id` - Node ID of the publishing gateway
|
|
||||||
|
|
||||||
#### Configuration Defaults (from `Default.h`)
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#define default_mqtt_address "mqtt.meshtastic.org"
|
|
||||||
#define default_mqtt_username "meshdev"
|
|
||||||
#define default_mqtt_password "large4cats"
|
|
||||||
#define default_mqtt_root "msh"
|
|
||||||
#define default_mqtt_encryption_enabled true
|
|
||||||
#define default_mqtt_tls_enabled false
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Key Concepts
|
|
||||||
|
|
||||||
- **Uplink** - Mesh packets sent TO the MQTT broker (controlled by `uplink_enabled` per channel)
|
|
||||||
- **Downlink** - MQTT messages received and injected INTO the mesh (controlled by `downlink_enabled` per channel)
|
|
||||||
- **Encryption** - When `encryption_enabled` is true, only encrypted packets are sent; plaintext JSON is disabled
|
|
||||||
- **ServiceEnvelope** - Protobuf wrapper containing packet + channel_id + gateway_id for routing
|
|
||||||
- **JSON Support** - Optional JSON encoding for integration with external systems (disabled on nRF52 by default)
|
|
||||||
|
|
||||||
#### PKI Messages
|
|
||||||
|
|
||||||
PKI (Public Key Infrastructure) messages have special handling:
|
|
||||||
|
|
||||||
- Accepted on a special "PKI" channel
|
|
||||||
- Allow encrypted DMs between nodes that discovered each other on downlink-enabled channels
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
firmware/
|
|
||||||
├── src/ # Main source code
|
|
||||||
│ ├── main.cpp # Application entry point
|
|
||||||
│ ├── mesh/ # Core mesh networking
|
|
||||||
│ │ ├── NodeDB.* # Node database management
|
|
||||||
│ │ ├── Router.* # Packet routing
|
|
||||||
│ │ ├── Channels.* # Channel management
|
|
||||||
│ │ ├── *Interface.* # Radio interface implementations
|
|
||||||
│ │ └── generated/ # Protobuf generated code
|
|
||||||
│ ├── modules/ # Feature modules (Position, Telemetry, etc.)
|
|
||||||
│ ├── gps/ # GPS handling
|
|
||||||
│ ├── graphics/ # Display drivers and UI
|
|
||||||
│ ├── platform/ # Platform-specific code
|
|
||||||
│ ├── input/ # Input device handling
|
|
||||||
│ └── concurrency/ # Threading utilities
|
|
||||||
├── variants/ # Hardware variant definitions
|
|
||||||
│ ├── esp32/ # ESP32 variants
|
|
||||||
│ ├── esp32s3/ # ESP32-S3 variants
|
|
||||||
│ ├── nrf52/ # nRF52 variants
|
|
||||||
│ └── rp2xxx/ # RP2040/RP2350 variants
|
|
||||||
├── protobufs/ # Protocol buffer definitions
|
|
||||||
├── boards/ # Custom PlatformIO board definitions
|
|
||||||
└── bin/ # Build and utility scripts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Coding Conventions
|
|
||||||
|
|
||||||
### General Style
|
|
||||||
|
|
||||||
- Follow existing code style - run `trunk fmt` before commits
|
|
||||||
- Prefer `LOG_DEBUG`, `LOG_INFO`, `LOG_WARN`, `LOG_ERROR` for logging
|
|
||||||
- Use `assert()` for invariants that should never fail
|
|
||||||
|
|
||||||
### Naming Conventions
|
|
||||||
|
|
||||||
- Classes: `PascalCase` (e.g., `PositionModule`, `NodeDB`)
|
|
||||||
- Functions/Methods: `camelCase` (e.g., `sendOurPosition`, `getNodeNum`)
|
|
||||||
- Constants/Defines: `UPPER_SNAKE_CASE` (e.g., `MAX_INTERVAL`, `ONE_DAY`)
|
|
||||||
- Member variables: `camelCase` (e.g., `lastGpsSend`, `nodeDB`)
|
|
||||||
- Config defines: `USERPREFS_*` for user-configurable options
|
|
||||||
|
|
||||||
### Key Patterns
|
|
||||||
|
|
||||||
#### Module System
|
|
||||||
|
|
||||||
Modules inherit from `MeshModule` or `ProtobufModule<T>` and implement:
|
|
||||||
|
|
||||||
- `handleReceivedProtobuf()` - Process incoming packets
|
|
||||||
- `allocReply()` - Generate response packets
|
|
||||||
- `runOnce()` - Periodic task execution (returns next run interval in ms)
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class MyModule : public ProtobufModule<meshtastic_MyMessage>
|
|
||||||
{
|
|
||||||
protected:
|
|
||||||
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg) override;
|
|
||||||
virtual int32_t runOnce() override;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Configuration Access
|
|
||||||
|
|
||||||
- `config.*` - Device configuration (LoRa, position, power, etc.)
|
|
||||||
- `moduleConfig.*` - Module-specific configuration
|
|
||||||
- `channels.*` - Channel configuration and management
|
|
||||||
|
|
||||||
#### Default Values
|
|
||||||
|
|
||||||
Use the `Default` class helpers in `src/mesh/Default.h`:
|
|
||||||
|
|
||||||
- `Default::getConfiguredOrDefaultMs(configured, default)` - Returns ms, using default if configured is 0
|
|
||||||
- `Default::getConfiguredOrMinimumValue(configured, min)` - Enforces minimum values
|
|
||||||
- `Default::getConfiguredOrDefaultMsScaled(configured, default, numNodes)` - Scales based on network size
|
|
||||||
|
|
||||||
#### Thread Safety
|
|
||||||
|
|
||||||
- Use `concurrency::Lock` for mutex protection
|
|
||||||
- Radio SPI access uses `SPILock`
|
|
||||||
- Prefer `OSThread` for background tasks
|
|
||||||
|
|
||||||
### Hardware Variants
|
|
||||||
|
|
||||||
Each hardware variant has:
|
|
||||||
|
|
||||||
- `variant.h` - Pin definitions and hardware capabilities
|
|
||||||
- `platformio.ini` - Build configuration
|
|
||||||
- Optional: `pins_arduino.h`, `rfswitch.h`
|
|
||||||
|
|
||||||
Key defines in variant.h:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#define USE_SX1262 // Radio chip selection
|
|
||||||
#define HAS_GPS 1 // Hardware capabilities
|
|
||||||
#define LORA_CS 36 // Pin assignments
|
|
||||||
#define SX126X_DIO1 14 // Radio-specific pins
|
|
||||||
```
|
|
||||||
|
|
||||||
### Protobuf Messages
|
|
||||||
|
|
||||||
- Defined in `protobufs/meshtastic/*.proto`
|
|
||||||
- Generated code in `src/mesh/generated/`
|
|
||||||
- Regenerate with `bin/regen-protos.sh`
|
|
||||||
- Message types prefixed with `meshtastic_`
|
|
||||||
|
|
||||||
### Conditional Compilation
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#if !MESHTASTIC_EXCLUDE_GPS // Feature exclusion
|
|
||||||
#ifdef ARCH_ESP32 // Architecture-specific
|
|
||||||
#if defined(USE_SX1262) // Radio-specific
|
|
||||||
#ifdef HAS_SCREEN // Hardware capability
|
|
||||||
#if USERPREFS_EVENT_MODE // User preferences
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build System
|
|
||||||
|
|
||||||
Uses **PlatformIO** with custom scripts:
|
|
||||||
|
|
||||||
- `bin/platformio-pre.py` - Pre-build script
|
|
||||||
- `bin/platformio-custom.py` - Custom build logic
|
|
||||||
|
|
||||||
Build commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pio run -e tbeam # Build specific target
|
|
||||||
pio run -e tbeam -t upload # Build and upload
|
|
||||||
pio run -e native # Build native/Linux version
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Tasks
|
|
||||||
|
|
||||||
### Adding a New Module
|
|
||||||
|
|
||||||
1. Create `src/modules/MyModule.cpp` and `.h`
|
|
||||||
2. Inherit from appropriate base class
|
|
||||||
3. Register in `src/modules/Modules.cpp`
|
|
||||||
4. Add protobuf messages if needed in `protobufs/`
|
|
||||||
|
|
||||||
### Adding a New Hardware Variant
|
|
||||||
|
|
||||||
1. Create directory under `variants/<arch>/<name>/`
|
|
||||||
2. Add `variant.h` with pin definitions
|
|
||||||
3. Add `platformio.ini` with build config
|
|
||||||
4. Reference common configs with `extends`
|
|
||||||
|
|
||||||
### Modifying Configuration Defaults
|
|
||||||
|
|
||||||
- Check `src/mesh/Default.h` for default value defines
|
|
||||||
- Check `src/mesh/NodeDB.cpp` for initialization logic
|
|
||||||
- Consider `isDefaultChannel()` checks for public channel restrictions
|
|
||||||
|
|
||||||
## Important Considerations
|
|
||||||
|
|
||||||
### Traffic Management
|
|
||||||
|
|
||||||
The mesh network has limited bandwidth. When modifying broadcast intervals:
|
|
||||||
|
|
||||||
- Respect minimum intervals on default/public channels
|
|
||||||
- Use `Default::getConfiguredOrMinimumValue()` to enforce minimums
|
|
||||||
- Consider `numOnlineNodes` scaling for congestion control
|
|
||||||
|
|
||||||
### Power Management
|
|
||||||
|
|
||||||
Many devices are battery-powered:
|
|
||||||
|
|
||||||
- Use `IF_ROUTER(routerVal, normalVal)` for role-based defaults
|
|
||||||
- Check `config.power.is_power_saving` for power-saving modes
|
|
||||||
- Implement proper `sleep()` methods in radio interfaces
|
|
||||||
|
|
||||||
### Channel Security
|
|
||||||
|
|
||||||
- `channels.isDefaultChannel(index)` - Check if using default/public settings
|
|
||||||
- Default channels get stricter rate limits to prevent abuse
|
|
||||||
- Private channels may have relaxed limits
|
|
||||||
|
|
||||||
## GitHub Actions CI/CD
|
|
||||||
|
|
||||||
The project uses GitHub Actions extensively for CI/CD. Key workflows are in `.github/workflows/`:
|
|
||||||
|
|
||||||
### Core CI Workflows
|
|
||||||
|
|
||||||
- **`main_matrix.yml`** - Main CI pipeline, runs on push to `master`/`develop` and PRs
|
|
||||||
- Uses `bin/generate_ci_matrix.py` to dynamically generate build targets
|
|
||||||
- Builds all supported hardware variants
|
|
||||||
- PRs build a subset (`--level pr`) for faster feedback
|
|
||||||
|
|
||||||
- **`trunk_check.yml`** - Code quality checks on PRs
|
|
||||||
- Runs Trunk.io for linting and formatting
|
|
||||||
- Must pass before merge
|
|
||||||
|
|
||||||
- **`tests.yml`** - End-to-end and hardware tests
|
|
||||||
- Runs daily on schedule
|
|
||||||
- Includes native tests and hardware-in-the-loop testing
|
|
||||||
|
|
||||||
- **`test_native.yml`** - Native platform unit tests
|
|
||||||
- Runs `pio test -e native`
|
|
||||||
|
|
||||||
### Release Workflows
|
|
||||||
|
|
||||||
- **`release_channels.yml`** - Triggered on GitHub release publish
|
|
||||||
- Builds Docker images
|
|
||||||
- Packages for PPA (Ubuntu), OBS (openSUSE), and COPR (Fedora)
|
|
||||||
- Handles Alpha/Beta/Stable release channels
|
|
||||||
|
|
||||||
- **`nightly.yml`** - Nightly builds from develop branch
|
|
||||||
|
|
||||||
- **`docker_build.yml`** / **`docker_manifest.yml`** - Docker image builds
|
|
||||||
|
|
||||||
### Build Matrix Generation
|
|
||||||
|
|
||||||
The CI uses `bin/generate_ci_matrix.py` to dynamically select which targets to build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate full build matrix
|
|
||||||
./bin/generate_ci_matrix.py all
|
|
||||||
|
|
||||||
# Generate PR-level matrix (subset for faster builds)
|
|
||||||
./bin/generate_ci_matrix.py all --level pr
|
|
||||||
```
|
|
||||||
|
|
||||||
Variants can specify their support level in `platformio.ini`:
|
|
||||||
|
|
||||||
- `custom_meshtastic_support_level = 1` - Actively supported, built on every PR
|
|
||||||
- `custom_meshtastic_support_level = 2` - Supported, built on merge to main branches
|
|
||||||
- `board_level = extra` - Extra builds, only on full releases
|
|
||||||
|
|
||||||
### Running Workflows Locally
|
|
||||||
|
|
||||||
Most workflows can be triggered manually via `workflow_dispatch` for testing.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
- Unit tests in `test/` directory
|
|
||||||
- Run with `pio test -e native`
|
|
||||||
- Use `bin/test-simulator.sh` for simulation testing
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [Documentation](https://meshtastic.org/docs/)
|
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
PKG_VERSION: ${{ steps.version.outputs.deb }}
|
PKG_VERSION: ${{ steps.version.outputs.deb }}
|
||||||
|
|
||||||
- name: Store binaries as an artifact
|
- name: Store binaries as an artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
# Use 'arctastic' self-hosted runner pool when building in the main repo
|
# Use 'arctastic' self-hosted runner pool when building in the main repo
|
||||||
runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }}
|
runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }}
|
||||||
outputs:
|
outputs:
|
||||||
artifact-id: ${{ steps.upload-firmware.outputs.artifact-id }}
|
artifact-id: ${{ steps.upload.outputs.artifact-id }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
@@ -29,6 +29,23 @@ jobs:
|
|||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
|
||||||
|
- name: Set OTA firmware source and target
|
||||||
|
if: startsWith(inputs.platform, 'esp32')
|
||||||
|
id: ota_dir
|
||||||
|
env:
|
||||||
|
PIO_PLATFORM: ${{ inputs.platform }}
|
||||||
|
run: |
|
||||||
|
if [ "$PIO_PLATFORM" = "esp32s3" ]; then
|
||||||
|
echo "src=firmware-s3.bin" >> $GITHUB_OUTPUT
|
||||||
|
echo "tgt=release/bleota-s3.bin" >> $GITHUB_OUTPUT
|
||||||
|
elif [ "$PIO_PLATFORM" = "esp32c3" ] || [ "$PIO_PLATFORM" = "esp32c6" ]; then
|
||||||
|
echo "src=firmware-c3.bin" >> $GITHUB_OUTPUT
|
||||||
|
echo "tgt=release/bleota-c3.bin" >> $GITHUB_OUTPUT
|
||||||
|
elif [ "$PIO_PLATFORM" = "esp32" ]; then
|
||||||
|
echo "src=firmware.bin" >> $GITHUB_OUTPUT
|
||||||
|
echo "tgt=release/bleota.bin" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build ${{ inputs.platform }}
|
- name: Build ${{ inputs.platform }}
|
||||||
id: build
|
id: build
|
||||||
uses: meshtastic/gh-action-firmware@main
|
uses: meshtastic/gh-action-firmware@main
|
||||||
@@ -36,83 +53,23 @@ jobs:
|
|||||||
pio_platform: ${{ inputs.platform }}
|
pio_platform: ${{ inputs.platform }}
|
||||||
pio_env: ${{ inputs.pio_env }}
|
pio_env: ${{ inputs.pio_env }}
|
||||||
pio_target: build
|
pio_target: build
|
||||||
|
ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
|
||||||
|
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
|
||||||
|
|
||||||
- name: ESP32 - Download Unified OTA firmware
|
- name: Echo manifest from release/firmware-*.mt.json to job summary
|
||||||
# Currently only esp32 and esp32s3 use the unified ota
|
if: ${{ always() }}
|
||||||
if: inputs.platform == 'esp32' || inputs.platform == 'esp32s3'
|
|
||||||
id: dl-ota-unified
|
|
||||||
env:
|
|
||||||
PIO_PLATFORM: ${{ inputs.platform }}
|
|
||||||
PIO_ENV: ${{ inputs.pio_env }}
|
|
||||||
OTA_URL: https://github.com/meshtastic/esp32-unified-ota/releases/latest/download/mt-${{ inputs.platform }}-ota.bin
|
|
||||||
working-directory: release
|
|
||||||
run: |
|
|
||||||
curl -L -o "mt-$PIO_PLATFORM-ota.bin" $OTA_URL
|
|
||||||
|
|
||||||
- name: ESP32-C* - Download BLE-Only OTA firmware
|
|
||||||
if: inputs.platform == 'esp32c3' || inputs.platform == 'esp32c6'
|
|
||||||
id: dl-ota-ble
|
|
||||||
env:
|
|
||||||
PIO_ENV: ${{ inputs.pio_env }}
|
|
||||||
OTA_URL: https://github.com/meshtastic/firmware-ota/releases/latest/download/firmware-c3.bin
|
|
||||||
working-directory: release
|
|
||||||
run: |
|
|
||||||
curl -L -o bleota-c3.bin $OTA_URL
|
|
||||||
|
|
||||||
- name: Update manifest with OTA file
|
|
||||||
if: inputs.platform == 'esp32' || inputs.platform == 'esp32s3' || inputs.platform == 'esp32c3' || inputs.platform == 'esp32c6'
|
|
||||||
working-directory: release
|
|
||||||
env:
|
|
||||||
PIO_PLATFORM: ${{ inputs.platform }}
|
|
||||||
run: |
|
|
||||||
# Determine OTA filename based on platform
|
|
||||||
if [[ "$PIO_PLATFORM" == "esp32" || "$PIO_PLATFORM" == "esp32s3" ]]; then
|
|
||||||
OTA_FILE="mt-${PIO_PLATFORM}-ota.bin"
|
|
||||||
else
|
|
||||||
OTA_FILE="bleota-c3.bin"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if OTA file exists
|
|
||||||
if [[ ! -f "$OTA_FILE" ]]; then
|
|
||||||
echo "OTA file $OTA_FILE not found, skipping manifest update"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Calculate MD5 and size
|
|
||||||
if command -v md5sum &> /dev/null; then
|
|
||||||
OTA_MD5=$(md5sum "$OTA_FILE" | cut -d' ' -f1)
|
|
||||||
else
|
|
||||||
OTA_MD5=$(md5 -q "$OTA_FILE")
|
|
||||||
fi
|
|
||||||
OTA_SIZE=$(stat -f%z "$OTA_FILE" 2>/dev/null || stat -c%s "$OTA_FILE")
|
|
||||||
|
|
||||||
# Find and update manifest file
|
|
||||||
for manifest in firmware-*.mt.json; do
|
|
||||||
if [[ -f "$manifest" ]]; then
|
|
||||||
echo "Updating $manifest with $OTA_FILE (md5: $OTA_MD5, size: $OTA_SIZE)"
|
|
||||||
# Add OTA entry to files array if not already present
|
|
||||||
jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" --arg part "app1" \
|
|
||||||
'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes, "part_name": $part}] else . end' \
|
|
||||||
"$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Job summary
|
|
||||||
env:
|
env:
|
||||||
PIO_ENV: ${{ inputs.pio_env }}
|
PIO_ENV: ${{ inputs.pio_env }}
|
||||||
run: |
|
run: |
|
||||||
echo "## $PIO_ENV" >> $GITHUB_STEP_SUMMARY
|
echo "## Manifest: \`$PIO_ENV\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "<details><summary><strong>Manifest</strong></summary>" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo '' >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||||
cat release/firmware-*.mt.json >> $GITHUB_STEP_SUMMARY
|
cat release/firmware-*.mt.json >> $GITHUB_STEP_SUMMARY
|
||||||
echo '' >> $GITHUB_STEP_SUMMARY
|
echo '' >> $GITHUB_STEP_SUMMARY
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Store binaries as an artifact
|
- name: Store binaries as an artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
id: upload-firmware
|
id: upload
|
||||||
with:
|
with:
|
||||||
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
|
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -125,12 +82,3 @@ jobs:
|
|||||||
release/*.zip
|
release/*.zip
|
||||||
release/device-*.sh
|
release/device-*.sh
|
||||||
release/device-*.bat
|
release/device-*.bat
|
||||||
|
|
||||||
- name: Store manifests as an artifact
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
id: upload-manifest
|
|
||||||
with:
|
|
||||||
name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
|
|
||||||
overwrite: true
|
|
||||||
path: |
|
|
||||||
release/*.mt.json
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ jobs:
|
|||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: ./
|
path: ./
|
||||||
pattern: firmware-*-*
|
pattern: firmware-*-*
|
||||||
@@ -111,7 +111,7 @@ jobs:
|
|||||||
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||||
|
|
||||||
- name: Repackage in single firmware zip
|
- name: Repackage in single firmware zip
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
|
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -127,7 +127,7 @@ jobs:
|
|||||||
./Meshtastic_nRF52_factory_erase*.uf2
|
./Meshtastic_nRF52_factory_erase*.uf2
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pattern: firmware-*-${{ needs.version.outputs.long }}
|
pattern: firmware-*-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -146,7 +146,7 @@ jobs:
|
|||||||
run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output
|
run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output
|
||||||
|
|
||||||
- name: Repackage in single elfs zip
|
- name: Repackage in single elfs zip
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
|
name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
name: Welcome First-Time Contributor
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: opened
|
|
||||||
pull_request_target:
|
|
||||||
types: opened
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
welcome:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write # Required to post comments and labels on issues
|
|
||||||
pull-requests: write # Required to post comments and labels on PRs
|
|
||||||
steps:
|
|
||||||
- uses: plbstl/first-contribution@v4
|
|
||||||
with:
|
|
||||||
labels: first-contribution
|
|
||||||
issue-opened-msg: |
|
|
||||||
### @{fc-author}, Welcome to Meshtastic! :wave:
|
|
||||||
|
|
||||||
Thanks for opening your first issue. If it's helpful, an easy way
|
|
||||||
to get logs is the "Open Serial Monitor" button on the [Web Flasher](https://flasher.meshtastic.org).
|
|
||||||
|
|
||||||
If you have ideas for features, note that we often debate big ideas
|
|
||||||
in the [discussions tab](https://github.com/meshtastic/firmware/discussions/categories/ideas)
|
|
||||||
first. This tracker tends to be for ideas that have community
|
|
||||||
consensus and a clear implementation.
|
|
||||||
|
|
||||||
We're very active [on discord](https://discord.com/invite/meshtastic),
|
|
||||||
especially the \#firmware and \#alphanauts-testing channels. If you'll
|
|
||||||
be around for a while, we'd love to see you there!
|
|
||||||
|
|
||||||
Welcome to the community! :heart:
|
|
||||||
|
|
||||||
pr-opened-msg: |
|
|
||||||
### @{fc-author}, Welcome to Meshtastic!
|
|
||||||
|
|
||||||
Thanks for opening your first pull request. We really appreciate it.
|
|
||||||
|
|
||||||
We discuss work as a team in discord, please join us in the [#firmware channel](https://discord.com/invite/meshtastic).
|
|
||||||
There's a big backlog of patches at the moment. If you have time,
|
|
||||||
please help us with some code review and testing of [other PRs](https://github.com/meshtastic/firmware/pulls)!
|
|
||||||
|
|
||||||
Welcome to the team :smile:
|
|
||||||
@@ -8,9 +8,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
- pioarduino # Remove when merged // use `feature/` in the future.
|
|
||||||
- event/*
|
- event/*
|
||||||
- feature/*
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**.md"
|
- "**.md"
|
||||||
- version.properties
|
- version.properties
|
||||||
@@ -20,9 +18,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
- pioarduino # Remove when merged // use `feature/` in the future.
|
|
||||||
- event/*
|
- event/*
|
||||||
- feature/*
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**.md"
|
- "**.md"
|
||||||
#- "**.yml"
|
#- "**.yml"
|
||||||
@@ -81,21 +77,16 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
check: ${{ fromJson(needs.setup.outputs.check) }}
|
check: ${{ fromJson(needs.setup.outputs.check) }}
|
||||||
# Use 'arctastic' self-hosted runner pool when checking in the main repo
|
|
||||||
runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }}
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
|
if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
- name: Build base
|
||||||
submodules: recursive
|
id: base
|
||||||
ref: ${{github.event.pull_request.head.ref}}
|
uses: ./.github/actions/setup-base
|
||||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
|
||||||
- name: Check ${{ matrix.check.board }}
|
- name: Check ${{ matrix.check.board }}
|
||||||
uses: meshtastic/gh-action-firmware@main
|
run: bin/check-all.sh ${{ matrix.check.board }}
|
||||||
with:
|
|
||||||
pio_platform: ${{ matrix.check.platform }}
|
|
||||||
pio_env: ${{ matrix.check.board }}
|
|
||||||
pio_target: check
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: [setup, version]
|
needs: [setup, version]
|
||||||
@@ -177,7 +168,7 @@ jobs:
|
|||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: ./
|
path: ./
|
||||||
pattern: firmware-${{matrix.arch}}-*
|
pattern: firmware-${{matrix.arch}}-*
|
||||||
@@ -187,7 +178,7 @@ jobs:
|
|||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Repackage in single firmware zip
|
- name: Repackage in single firmware zip
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -201,11 +192,10 @@ jobs:
|
|||||||
./device-*.bat
|
./device-*.bat
|
||||||
./littlefs-*.bin
|
./littlefs-*.bin
|
||||||
./bleota*bin
|
./bleota*bin
|
||||||
./mt-*-ota.bin
|
|
||||||
./Meshtastic_nRF52_factory_erase*.uf2
|
./Meshtastic_nRF52_factory_erase*.uf2
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -224,7 +214,7 @@ jobs:
|
|||||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||||
|
|
||||||
- name: Repackage in single elfs zip
|
- name: Repackage in single elfs zip
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -238,48 +228,6 @@ jobs:
|
|||||||
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
|
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
shame:
|
|
||||||
if: github.repository == 'meshtastic/firmware'
|
|
||||||
continue-on-error: true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [build]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
if: github.event_name == 'pull_request_target'
|
|
||||||
with:
|
|
||||||
filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head)
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Download the current manifests
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
path: ./manifests-new/
|
|
||||||
pattern: manifest-*
|
|
||||||
merge-multiple: true
|
|
||||||
- name: Upload combined manifests for later commit and global stats crunching.
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
id: upload-manifest
|
|
||||||
with:
|
|
||||||
name: manifests-${{ github.sha }}
|
|
||||||
overwrite: true
|
|
||||||
path: manifests-new/*.mt.json
|
|
||||||
- name: Find the merge base
|
|
||||||
if: github.event_name == 'pull_request_target'
|
|
||||||
run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV
|
|
||||||
env:
|
|
||||||
base: ${{ github.base_ref }}
|
|
||||||
head: ${{ github.sha }}
|
|
||||||
# Currently broken (for-loop through EVERY artifact -- rate limiting)
|
|
||||||
# - name: Download the old manifests
|
|
||||||
# if: github.event_name == 'pull_request_target'
|
|
||||||
# run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/
|
|
||||||
# env:
|
|
||||||
# GH_TOKEN: ${{ github.token }}
|
|
||||||
# merge_base: ${{ env.MERGE_BASE }}
|
|
||||||
# repo: ${{ github.repository }}
|
|
||||||
# - name: Do scan and post comment
|
|
||||||
# if: github.event_name == 'pull_request_target'
|
|
||||||
# run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/
|
|
||||||
|
|
||||||
release-artifacts:
|
release-artifacts:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
|
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
|
||||||
@@ -294,24 +242,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: 3.x
|
|
||||||
|
|
||||||
- name: Generate release notes
|
|
||||||
id: release_notes
|
|
||||||
run: |
|
|
||||||
chmod +x ./bin/generate_release_notes.py
|
|
||||||
NOTES=$(./bin/generate_release_notes.py ${{ needs.version.outputs.long }})
|
|
||||||
echo "notes<<EOF" >> $GITHUB_OUTPUT
|
|
||||||
echo "$NOTES" >> $GITHUB_OUTPUT
|
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
@@ -321,17 +251,18 @@ jobs:
|
|||||||
prerelease: true
|
prerelease: true
|
||||||
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
|
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
|
||||||
tag_name: v${{ needs.version.outputs.long }}
|
tag_name: v${{ needs.version.outputs.long }}
|
||||||
body: ${{ steps.release_notes.outputs.notes }}
|
body: |
|
||||||
|
Autogenerated by github action, developer should edit as required before publishing...
|
||||||
|
|
||||||
- name: Download source deb
|
- name: Download source deb
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
|
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./output/debian-src
|
path: ./output/debian-src
|
||||||
|
|
||||||
- name: Download `native-tft` pio deps
|
- name: Download `native-tft` pio deps
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -355,7 +286,7 @@ jobs:
|
|||||||
}' > firmware-${{ needs.version.outputs.long }}.json
|
}' > firmware-${{ needs.version.outputs.long }}.json
|
||||||
|
|
||||||
- name: Save Release manifest artifact
|
- name: Save Release manifest artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: manifest-${{ needs.version.outputs.long }}
|
name: manifest-${{ needs.version.outputs.long }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -396,7 +327,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -413,7 +344,7 @@ jobs:
|
|||||||
- name: Zip firmware
|
- name: Zip firmware
|
||||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -445,8 +376,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
@@ -454,25 +383,18 @@ jobs:
|
|||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
- name: Get firmware artifacts
|
- name: Get firmware artifacts
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./publish
|
path: ./publish
|
||||||
|
|
||||||
- name: Get manifest artifact
|
- name: Get manifest artifact
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pattern: manifest-${{ needs.version.outputs.long }}
|
pattern: manifest-${{ needs.version.outputs.long }}
|
||||||
path: ./publish
|
path: ./publish
|
||||||
|
|
||||||
- name: Generate release notes
|
|
||||||
run: |
|
|
||||||
chmod +x ./bin/generate_release_notes.py
|
|
||||||
./bin/generate_release_notes.py ${{ needs.version.outputs.long }} > ./publish/release_notes.md
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Publish firmware to meshtastic.github.io
|
- name: Publish firmware to meshtastic.github.io
|
||||||
uses: peaceiris/actions-gh-pages@v4
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: ./
|
path: ./
|
||||||
pattern: firmware-${{matrix.arch}}-*
|
pattern: firmware-${{matrix.arch}}-*
|
||||||
@@ -160,7 +160,7 @@ jobs:
|
|||||||
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||||
|
|
||||||
- name: Repackage in single firmware zip
|
- name: Repackage in single firmware zip
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -176,7 +176,7 @@ jobs:
|
|||||||
./Meshtastic_nRF52_factory_erase*.uf2
|
./Meshtastic_nRF52_factory_erase*.uf2
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -195,7 +195,7 @@ jobs:
|
|||||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||||
|
|
||||||
- name: Repackage in single elfs zip
|
- name: Repackage in single elfs zip
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -235,14 +235,14 @@ jobs:
|
|||||||
Autogenerated by github action, developer should edit as required before publishing...
|
Autogenerated by github action, developer should edit as required before publishing...
|
||||||
|
|
||||||
- name: Download source deb
|
- name: Download source deb
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
|
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./output/debian-src
|
path: ./output/debian-src
|
||||||
|
|
||||||
- name: Download `native-tft` pio deps
|
- name: Download `native-tft` pio deps
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -292,7 +292,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -309,7 +309,7 @@ jobs:
|
|||||||
- name: Zip firmware
|
- name: Zip firmware
|
||||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -347,7 +347,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
name: Issue Triage (Models)
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
models: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
triage:
|
|
||||||
if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
- name: Detect spam or low-quality content
|
|
||||||
uses: actions/ai-inference@v2
|
|
||||||
id: quality
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
max-tokens: 20
|
|
||||||
prompt: |
|
|
||||||
Is this GitHub issue spam, AI-generated slop, or low quality?
|
|
||||||
|
|
||||||
Title: ${{ github.event.issue.title }}
|
|
||||||
Body: ${{ github.event.issue.body }}
|
|
||||||
|
|
||||||
Respond with exactly one of: spam, ai-generated, needs-review, ok
|
|
||||||
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
|
|
||||||
model: openai/gpt-4o-mini
|
|
||||||
|
|
||||||
- name: Apply quality label if needed
|
|
||||||
if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
env:
|
|
||||||
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
|
|
||||||
const labelMeta = {
|
|
||||||
'spam': { color: 'd73a4a', description: 'Possible spam' },
|
|
||||||
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
|
|
||||||
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
|
|
||||||
};
|
|
||||||
const meta = labelMeta[label];
|
|
||||||
if (!meta) return;
|
|
||||||
|
|
||||||
// Ensure label exists
|
|
||||||
try {
|
|
||||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
|
||||||
} catch (e) {
|
|
||||||
if (e.status !== 404) throw e;
|
|
||||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply label
|
|
||||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
|
|
||||||
|
|
||||||
// Set output to skip remaining steps
|
|
||||||
core.setOutput('is_spam', 'true');
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# Step 2: Duplicate detection - only if not spam
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
- name: Detect duplicate issues
|
|
||||||
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
|
|
||||||
uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# Step 3: Completeness check + auto-labeling (combined into one AI call)
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
- name: Determine if completeness check should be skipped
|
|
||||||
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
id: check-skip
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const title = (context.payload.issue.title || '').toLowerCase();
|
|
||||||
const labels = (context.payload.issue.labels || []).map(label => label.name);
|
|
||||||
const hasFeatureRequest = title.includes('feature request');
|
|
||||||
const hasEnhancement = labels.includes('enhancement');
|
|
||||||
const shouldSkip = hasFeatureRequest && hasEnhancement;
|
|
||||||
core.setOutput('should_skip', shouldSkip ? 'true' : 'false');
|
|
||||||
|
|
||||||
- name: Analyze issue completeness and determine labels
|
|
||||||
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true'
|
|
||||||
uses: actions/ai-inference@v2
|
|
||||||
id: analysis
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
prompt: |
|
|
||||||
Analyze this GitHub issue for completeness and determine if it needs labels.
|
|
||||||
|
|
||||||
IMPORTANT: Distinguish between:
|
|
||||||
- Device/firmware bugs (crashes, reboots, lockups, radio/GPS/display/power issues) - these need device logs
|
|
||||||
- Build/release/packaging issues (missing files, CI failures, download problems) - these do NOT need device logs
|
|
||||||
- Documentation or website issues - these do NOT need device logs
|
|
||||||
|
|
||||||
If this is a device/firmware bug, request device logs and explain how to get them:
|
|
||||||
|
|
||||||
Web Flasher logs:
|
|
||||||
- Go to https://flasher.meshtastic.org
|
|
||||||
- Connect the device via USB and click Connect
|
|
||||||
- Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs
|
|
||||||
|
|
||||||
Meshtastic CLI logs:
|
|
||||||
- Run: meshtastic --port <serial-port> --noproto
|
|
||||||
- Reproduce the problem, then copy/paste the terminal output
|
|
||||||
|
|
||||||
Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual.
|
|
||||||
|
|
||||||
Respond ONLY with valid JSON (no markdown, no code fences):
|
|
||||||
{"complete": true, "comment": "", "label": "none"}
|
|
||||||
OR
|
|
||||||
{"complete": false, "comment": "Your helpful comment", "label": "needs-logs"}
|
|
||||||
|
|
||||||
Use "needs-logs" ONLY if this is a device/firmware bug AND no logs are attached.
|
|
||||||
Use "needs-info" if basic info like firmware version or steps to reproduce are missing.
|
|
||||||
Use "none" if the issue is complete, is a feature request, or is a build/CI/packaging issue.
|
|
||||||
|
|
||||||
Title: ${{ github.event.issue.title }}
|
|
||||||
Body: ${{ github.event.issue.body }}
|
|
||||||
system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. Only request device logs for actual device/firmware bugs, not for build/release/CI issues.
|
|
||||||
model: openai/gpt-4o-mini
|
|
||||||
|
|
||||||
- name: Process analysis result
|
|
||||||
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != ''
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
id: process
|
|
||||||
env:
|
|
||||||
AI_RESPONSE: ${{ steps.analysis.outputs.response }}
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
let raw = (process.env.AI_RESPONSE || '').trim();
|
|
||||||
|
|
||||||
// Strip markdown code fences if present
|
|
||||||
raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
|
|
||||||
|
|
||||||
let complete = true;
|
|
||||||
let comment = '';
|
|
||||||
let label = 'none';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
complete = !!parsed.complete;
|
|
||||||
comment = (parsed.comment ?? '').toString().trim();
|
|
||||||
label = (parsed.label ?? 'none').toString().trim().toLowerCase();
|
|
||||||
} catch {
|
|
||||||
// If JSON parse fails, log warning and don't comment (avoid posting raw JSON)
|
|
||||||
console.log('Failed to parse AI response as JSON:', raw);
|
|
||||||
complete = true;
|
|
||||||
comment = '';
|
|
||||||
label = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate label
|
|
||||||
const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']);
|
|
||||||
if (!allowedLabels.has(label)) label = 'none';
|
|
||||||
|
|
||||||
// Only comment if we have a valid parsed comment (not raw JSON)
|
|
||||||
const shouldComment = !complete && comment.length > 0 && !comment.startsWith('{');
|
|
||||||
core.setOutput('should_comment', shouldComment ? 'true' : 'false');
|
|
||||||
core.setOutput('comment_body', comment);
|
|
||||||
core.setOutput('label', label);
|
|
||||||
|
|
||||||
- name: Apply triage label
|
|
||||||
if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none'
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
env:
|
|
||||||
LABEL_NAME: ${{ steps.process.outputs.label }}
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const label = process.env.LABEL_NAME;
|
|
||||||
const labelMeta = {
|
|
||||||
'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' },
|
|
||||||
'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' },
|
|
||||||
};
|
|
||||||
const meta = labelMeta[label];
|
|
||||||
if (!meta) return;
|
|
||||||
|
|
||||||
// Ensure label exists
|
|
||||||
try {
|
|
||||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
|
||||||
} catch (e) {
|
|
||||||
if (e.status !== 404) throw e;
|
|
||||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply label
|
|
||||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
|
|
||||||
|
|
||||||
- name: Comment on issue
|
|
||||||
if: steps.process.outputs.should_comment == 'true'
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
env:
|
|
||||||
COMMENT_BODY: ${{ steps.process.outputs.comment_body }}
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: context.payload.issue.number,
|
|
||||||
body: process.env.COMMENT_BODY
|
|
||||||
});
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
name: PR Triage (Models)
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
models: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
triage:
|
|
||||||
if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# Step 1: Check if PR already has automation/type labels (skip if so)
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
- name: Check existing labels
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
id: check-labels
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const skipLabels = new Set(['automation']);
|
|
||||||
const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']);
|
|
||||||
const prLabels = context.payload.pull_request.labels.map(l => l.name);
|
|
||||||
|
|
||||||
const shouldSkipAll = prLabels.some(l => skipLabels.has(l));
|
|
||||||
const hasTypeLabel = prLabels.some(l => typeLabels.has(l));
|
|
||||||
|
|
||||||
core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false');
|
|
||||||
core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false');
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# Step 2: Quality check (spam/AI-slop detection)
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
- name: Detect spam or low-quality content
|
|
||||||
if: steps.check-labels.outputs.skip_all != 'true'
|
|
||||||
uses: actions/ai-inference@v2
|
|
||||||
id: quality
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
max-tokens: 20
|
|
||||||
prompt: |
|
|
||||||
Is this GitHub pull request spam, AI-generated slop, or low quality?
|
|
||||||
|
|
||||||
Title: ${{ github.event.pull_request.title }}
|
|
||||||
Body: ${{ github.event.pull_request.body }}
|
|
||||||
|
|
||||||
Respond with exactly one of: spam, ai-generated, needs-review, ok
|
|
||||||
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
|
|
||||||
model: openai/gpt-4o-mini
|
|
||||||
|
|
||||||
- name: Apply quality label if needed
|
|
||||||
if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
id: quality-label
|
|
||||||
env:
|
|
||||||
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
|
|
||||||
const labelMeta = {
|
|
||||||
'spam': { color: 'd73a4a', description: 'Possible spam' },
|
|
||||||
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
|
|
||||||
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
|
|
||||||
};
|
|
||||||
const meta = labelMeta[label];
|
|
||||||
if (!meta) return;
|
|
||||||
|
|
||||||
// Ensure label exists
|
|
||||||
try {
|
|
||||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
|
||||||
} catch (e) {
|
|
||||||
if (e.status !== 404) throw e;
|
|
||||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply label
|
|
||||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
|
|
||||||
|
|
||||||
core.setOutput('is_spam', 'true');
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# Step 3: Auto-label PR type (bugfix/hardware-support/enhancement)
|
|
||||||
# Only skip for spam/ai-generated; still classify needs-review PRs
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
- name: Classify PR for labeling
|
|
||||||
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.quality.outputs.response != 'spam' && steps.quality.outputs.response != 'ai-generated'
|
|
||||||
uses: actions/ai-inference@v2
|
|
||||||
id: classify
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
max-tokens: 30
|
|
||||||
prompt: |
|
|
||||||
Classify this pull request into exactly one category.
|
|
||||||
|
|
||||||
Return exactly one of: bugfix, hardware-support, enhancement
|
|
||||||
|
|
||||||
Use bugfix if it fixes a bug, crash, or incorrect behavior.
|
|
||||||
Use hardware-support if it adds or improves support for a specific hardware device/variant.
|
|
||||||
Use enhancement if it adds a new feature, improves performance, or refactors code.
|
|
||||||
|
|
||||||
Title: ${{ github.event.pull_request.title }}
|
|
||||||
Body: ${{ github.event.pull_request.body }}
|
|
||||||
system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label.
|
|
||||||
model: openai/gpt-4o-mini
|
|
||||||
|
|
||||||
- name: Apply type label
|
|
||||||
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != ''
|
|
||||||
uses: actions/github-script@v8
|
|
||||||
env:
|
|
||||||
TYPE_LABEL: ${{ steps.classify.outputs.response }}
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const label = (process.env.TYPE_LABEL || '').trim().toLowerCase();
|
|
||||||
const labelMeta = {
|
|
||||||
'bugfix': { color: 'd73a4a', description: 'Bug fix' },
|
|
||||||
'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' },
|
|
||||||
'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' },
|
|
||||||
};
|
|
||||||
const meta = labelMeta[label];
|
|
||||||
if (!meta) return;
|
|
||||||
|
|
||||||
// Ensure label exists
|
|
||||||
try {
|
|
||||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
|
||||||
} catch (e) {
|
|
||||||
if (e.status !== 404) throw e;
|
|
||||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply label
|
|
||||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
|
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
PLATFORMIO_CORE_DIR: pio/core
|
PLATFORMIO_CORE_DIR: pio/core
|
||||||
|
|
||||||
- name: Store binaries as an artifact
|
- name: Store binaries as an artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }}
|
name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download test artifacts
|
- name: Download test artifacts
|
||||||
if: needs.native-tests.result != 'skipped'
|
if: needs.native-tests.result != 'skipped'
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: platformio-test-report-${{ steps.version.outputs.long }}
|
name: platformio-test-report-${{ steps.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
@@ -48,37 +48,6 @@ jobs:
|
|||||||
${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }}
|
${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
publish-release-notes:
|
|
||||||
if: github.event.action == 'published'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Get release version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
# Extract version from tag (e.g., v2.7.15.567b8ea -> 2.7.15.567b8ea)
|
|
||||||
VERSION=${GITHUB_REF#refs/tags/v}
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Get release notes
|
|
||||||
run: |
|
|
||||||
mkdir -p ./publish
|
|
||||||
gh release view ${{ github.event.release.tag_name }} --json body --jq '.body' > ./publish/release_notes.md
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Publish release notes to meshtastic.github.io
|
|
||||||
uses: peaceiris/actions-gh-pages@v4
|
|
||||||
with:
|
|
||||||
deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }}
|
|
||||||
external_repository: meshtastic/meshtastic.github.io
|
|
||||||
publish_branch: master
|
|
||||||
publish_dir: ./publish
|
|
||||||
destination_dir: firmware-${{ steps.version.outputs.version }}
|
|
||||||
user_name: github-actions[bot]
|
|
||||||
user_email: github-actions[bot]@users.noreply.github.com
|
|
||||||
commit_message: Release notes for ${{ steps.version.outputs.version }}
|
|
||||||
enable_jekyll: true
|
|
||||||
|
|
||||||
# Create a PR to bump version when a release is Published
|
# Create a PR to bump version when a release is Published
|
||||||
bump-version:
|
bump-version:
|
||||||
if: github.event.action == 'published'
|
if: github.event.action == 'published'
|
||||||
@@ -133,7 +102,7 @@ jobs:
|
|||||||
PIP_DISABLE_PIP_VERSION_CHECK: 1
|
PIP_DISABLE_PIP_VERSION_CHECK: 1
|
||||||
|
|
||||||
- name: Create Bumps pull request
|
- name: Create Bumps pull request
|
||||||
uses: peter-evans/create-pull-request@v8
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
base: ${{ github.event.repository.default_branch }}
|
base: ${{ github.event.repository.default_branch }}
|
||||||
branch: create-pull-request/bump-version
|
branch: create-pull-request/bump-version
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
# step 3
|
# step 3
|
||||||
- name: save report as pipeline artifact
|
- name: save report as pipeline artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: report.sarif
|
name: report.sarif
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
|
|
||||||
- name: Save coverage information
|
- name: Save coverage information
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
if: always() # run this step even if previous step failed
|
if: always() # run this step even if previous step failed
|
||||||
with:
|
with:
|
||||||
name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}
|
name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}
|
||||||
@@ -94,7 +94,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Save test results
|
- name: Save test results
|
||||||
if: always() # run this step even if previous step failed
|
if: always() # run this step even if previous step failed
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: platformio-test-report-${{ steps.version.outputs.long }}
|
name: platformio-test-report-${{ steps.version.outputs.long }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative.
|
sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative.
|
||||||
|
|
||||||
- name: Save coverage information
|
- name: Save coverage information
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
if: always() # run this step even if previous step failed
|
if: always() # run this step even if previous step failed
|
||||||
with:
|
with:
|
||||||
name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}
|
name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}
|
||||||
@@ -137,20 +137,20 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
|
|
||||||
- name: Download test artifacts
|
- name: Download test artifacts
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: platformio-test-report-${{ steps.version.outputs.long }}
|
name: platformio-test-report-${{ steps.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Test Report
|
- name: Test Report
|
||||||
uses: dorny/test-reporter@v2.5.0
|
uses: dorny/test-reporter@v2.3.0
|
||||||
with:
|
with:
|
||||||
name: PlatformIO Tests
|
name: PlatformIO Tests
|
||||||
path: testreport.xml
|
path: testreport.xml
|
||||||
reporter: java-junit
|
reporter: java-junit
|
||||||
|
|
||||||
- name: Download coverage artifacts
|
- name: Download coverage artifacts
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}
|
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}
|
||||||
path: code-coverage-report
|
path: code-coverage-report
|
||||||
@@ -163,7 +163,7 @@ jobs:
|
|||||||
genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report
|
genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report
|
||||||
|
|
||||||
- name: Save Code Coverage Report
|
- name: Save Code Coverage Report
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: code-coverage-report-${{ steps.version.outputs.long }}
|
name: code-coverage-report-${{ steps.version.outputs.long }}
|
||||||
path: code-coverage-report
|
path: code-coverage-report
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: Run Trunk Fmt on PR Comment
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trunk-fmt:
|
||||||
|
if: github.event.issue.pull_request != null && contains(github.event.comment.body, 'trunk fmt')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
|
||||||
|
- name: Install trunk
|
||||||
|
run: curl https://get.trunk.io -fsSL | bash
|
||||||
|
|
||||||
|
- name: Run Trunk Fmt
|
||||||
|
run: trunk fmt
|
||||||
|
|
||||||
|
- name: Get release version string
|
||||||
|
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||||
|
id: version
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add .
|
||||||
|
git commit -m "Add firmware version ${{ steps.version.outputs.long }}"
|
||||||
|
git push
|
||||||
|
|
||||||
|
- name: Comment on PR
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
github.issues.createComment({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: '`trunk fmt` has been run on this PR.'
|
||||||
|
})
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Update submodule
|
- name: Update submodule
|
||||||
if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }}
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
run: |
|
run: |
|
||||||
git submodule update --remote protobufs
|
git submodule update --remote protobufs
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
./bin/regen-protos.sh
|
./bin/regen-protos.sh
|
||||||
|
|
||||||
- name: Create pull request
|
- name: Create pull request
|
||||||
uses: peter-evans/create-pull-request@v8
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
branch: create-pull-request/update-protobufs
|
branch: create-pull-request/update-protobufs
|
||||||
labels: submodules
|
labels: submodules
|
||||||
|
|||||||
-12
@@ -41,15 +41,3 @@ src/mesh/raspihttp/private_key.pem
|
|||||||
|
|
||||||
# Ignore logo (set at build time with platformio-custom.py)
|
# Ignore logo (set at build time with platformio-custom.py)
|
||||||
data/boot/logo.*
|
data/boot/logo.*
|
||||||
|
|
||||||
# pioarduino platform
|
|
||||||
managed_components/*
|
|
||||||
arduino-lib-builder*
|
|
||||||
dependencies.lock
|
|
||||||
idf_component.yml
|
|
||||||
CMakeLists.txt
|
|
||||||
/sdkconfig.*
|
|
||||||
.dummy/*
|
|
||||||
|
|
||||||
# PYTHONPATH used by the Nix shell
|
|
||||||
.python3
|
|
||||||
|
|||||||
+12
-12
@@ -8,25 +8,25 @@ plugins:
|
|||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
lint:
|
lint:
|
||||||
enabled:
|
enabled:
|
||||||
- checkov@3.2.500
|
- checkov@3.2.495
|
||||||
- renovate@43.4.0
|
- renovate@42.30.4
|
||||||
- prettier@3.8.1
|
- prettier@3.7.4
|
||||||
- trufflehog@3.93.0
|
- trufflehog@3.91.2
|
||||||
- yamllint@1.38.0
|
- yamllint@1.37.1
|
||||||
- bandit@1.9.3
|
- bandit@1.9.2
|
||||||
- trivy@0.69.1
|
- trivy@0.67.2
|
||||||
- taplo@0.10.0
|
- taplo@0.10.0
|
||||||
- ruff@0.15.0
|
- ruff@0.14.7
|
||||||
- isort@7.0.0
|
- isort@7.0.0
|
||||||
- markdownlint@0.47.0
|
- markdownlint@0.46.0
|
||||||
- oxipng@10.1.0
|
- oxipng@9.1.5
|
||||||
- svgo@4.0.0
|
- svgo@4.0.0
|
||||||
- actionlint@1.7.10
|
- actionlint@1.7.9
|
||||||
- flake8@7.3.0
|
- flake8@7.3.0
|
||||||
- hadolint@2.14.0
|
- hadolint@2.14.0
|
||||||
- shfmt@3.6.0
|
- shfmt@3.6.0
|
||||||
- shellcheck@0.11.0
|
- shellcheck@0.11.0
|
||||||
- black@26.1.0
|
- black@25.11.0
|
||||||
- git-diff-check
|
- git-diff-check
|
||||||
- gitleaks@8.30.0
|
- gitleaks@8.30.0
|
||||||
- clang-format@16.0.3
|
- clang-format@16.0.3
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
|
|||||||
curl wget g++ zip git ca-certificates pkg-config \
|
curl wget g++ zip git ca-certificates pkg-config \
|
||||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev \
|
libx11-dev libinput-dev libxkbcommon-x11-dev \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
&& pip install --no-cache-dir -U platformio \
|
&& pip install --no-cache-dir -U platformio \
|
||||||
&& mkdir /tmp/firmware
|
&& mkdir /tmp/firmware
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
| Firmware Version | Supported |
|
| Firmware Version | Supported |
|
||||||
| ---------------- | ------------------ |
|
| ---------------- | ------------------ |
|
||||||
| 2.7.x | :white_check_mark: |
|
| 2.6.x | :white_check_mark: |
|
||||||
| <= 2.6.x | :x: |
|
| <= 2.5.x | :x: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ RUN apk --no-cache add \
|
|||||||
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
||||||
libgpiod-dev yaml-cpp-dev bluez-dev \
|
libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||||
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
||||||
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev \
|
libx11-dev libinput-dev libxkbcommon-dev \
|
||||||
&& rm -rf /var/cache/apk/* \
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& pip install --no-cache-dir -U platformio \
|
&& pip install --no-cache-dir -U platformio \
|
||||||
&& mkdir /tmp/firmware
|
&& mkdir /tmp/firmware
|
||||||
|
|||||||
+14
-4
@@ -22,7 +22,7 @@ export APP_VERSION=$VERSION
|
|||||||
|
|
||||||
basename=firmware-$1-$VERSION
|
basename=firmware-$1-$VERSION
|
||||||
|
|
||||||
pio run --environment $1 -t mtjson # -v
|
pio run --environment $1 # -v
|
||||||
|
|
||||||
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
||||||
|
|
||||||
@@ -32,10 +32,20 @@ cp $BUILDDIR/$basename.factory.bin $OUTDIR/$basename.factory.bin
|
|||||||
echo "Copying ESP32 update bin file"
|
echo "Copying ESP32 update bin file"
|
||||||
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
|
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
|
||||||
|
|
||||||
echo "Copying Filesystem for ESP32 targets"
|
echo "Building Filesystem for ESP32 targets"
|
||||||
|
# If you want to build the webui, uncomment the following lines
|
||||||
|
# pio run --environment $1 -t buildfs
|
||||||
|
# cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
|
||||||
|
# # Remove webserver files from the filesystem and rebuild
|
||||||
|
# ls -l data/static # Diagnostic list of files
|
||||||
|
# rm -rf data/static
|
||||||
|
pio run --environment $1 -t buildfs --disable-auto-clean
|
||||||
cp $BUILDDIR/littlefs-$1-$VERSION.bin $OUTDIR/littlefs-$1-$VERSION.bin
|
cp $BUILDDIR/littlefs-$1-$VERSION.bin $OUTDIR/littlefs-$1-$VERSION.bin
|
||||||
cp bin/device-install.* $OUTDIR/
|
cp bin/device-install.* $OUTDIR/
|
||||||
cp bin/device-update.* $OUTDIR/
|
cp bin/device-update.* $OUTDIR/
|
||||||
|
|
||||||
echo "Copying manifest"
|
# Generate the manifest file
|
||||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
echo "Generating Meshtastic manifest"
|
||||||
|
TIMEFORMAT="Generated manifest in %E seconds"
|
||||||
|
time pio run --environment $1 -t mtjson --silent --disable-auto-clean
|
||||||
|
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
|
||||||
|
|||||||
+7
-5
@@ -21,14 +21,13 @@ rm -f $BUILDDIR/firmware*
|
|||||||
export APP_VERSION=$VERSION
|
export APP_VERSION=$VERSION
|
||||||
|
|
||||||
basename=firmware-$1-$VERSION
|
basename=firmware-$1-$VERSION
|
||||||
ota_basename=${basename}-ota
|
|
||||||
|
|
||||||
pio run --environment $1 -t mtjson # -v
|
pio run --environment $1 # -v
|
||||||
|
|
||||||
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
||||||
|
|
||||||
echo "Copying NRF52 dfu (OTA) file"
|
echo "Copying NRF52 dfu (OTA) file"
|
||||||
cp $BUILDDIR/$basename.zip $OUTDIR/$ota_basename.zip
|
cp $BUILDDIR/$basename.zip $OUTDIR/$basename.zip
|
||||||
|
|
||||||
echo "Copying NRF52 UF2 file"
|
echo "Copying NRF52 UF2 file"
|
||||||
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
|
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
|
||||||
@@ -48,5 +47,8 @@ if (echo $1 | grep -q "rak4631"); then
|
|||||||
cp $SRCHEX $OUTDIR/
|
cp $SRCHEX $OUTDIR/
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Copying manifest"
|
# Generate the manifest file
|
||||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
echo "Generating Meshtastic manifest"
|
||||||
|
TIMEFORMAT="Generated manifest in %E seconds"
|
||||||
|
time pio run --environment $1 -t mtjson --silent --disable-auto-clean
|
||||||
|
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
|
||||||
|
|||||||
+6
-3
@@ -22,12 +22,15 @@ export APP_VERSION=$VERSION
|
|||||||
|
|
||||||
basename=firmware-$1-$VERSION
|
basename=firmware-$1-$VERSION
|
||||||
|
|
||||||
pio run --environment $1 -t mtjson # -v
|
pio run --environment $1 # -v
|
||||||
|
|
||||||
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
||||||
|
|
||||||
echo "Copying uf2 file"
|
echo "Copying uf2 file"
|
||||||
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
|
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
|
||||||
|
|
||||||
echo "Copying manifest"
|
# Generate the manifest file
|
||||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
echo "Generating Meshtastic manifest"
|
||||||
|
TIMEFORMAT="Generated manifest in %E seconds"
|
||||||
|
time pio run --environment $1 -t mtjson --silent --disable-auto-clean
|
||||||
|
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
|
||||||
|
|||||||
@@ -22,12 +22,15 @@ export APP_VERSION=$VERSION
|
|||||||
|
|
||||||
basename=firmware-$1-$VERSION
|
basename=firmware-$1-$VERSION
|
||||||
|
|
||||||
pio run --environment $1 -t mtjson # -v
|
pio run --environment $1 # -v
|
||||||
|
|
||||||
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
|
||||||
|
|
||||||
echo "Copying STM32 bin file"
|
echo "Copying STM32 bin file"
|
||||||
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
|
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
|
||||||
|
|
||||||
echo "Copying manifest"
|
# Generate the manifest file
|
||||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
echo "Generating Meshtastic manifest"
|
||||||
|
TIMEFORMAT="Generated manifest in %E seconds"
|
||||||
|
time pio run --environment $1 -t mtjson --silent --disable-auto-clean
|
||||||
|
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
|
||||||
|
|||||||
@@ -105,8 +105,6 @@ Lora:
|
|||||||
|
|
||||||
GPS:
|
GPS:
|
||||||
# SerialPath: /dev/ttyS0
|
# SerialPath: /dev/ttyS0
|
||||||
# ExtraPins:
|
|
||||||
# - 22
|
|
||||||
|
|
||||||
### Specify I2C device, or leave blank for none
|
### Specify I2C device, or leave blank for none
|
||||||
|
|
||||||
@@ -186,8 +184,6 @@ Input:
|
|||||||
Logging:
|
Logging:
|
||||||
LogLevel: info # debug, info, warn, error
|
LogLevel: info # debug, info, warn, error
|
||||||
# TraceFile: /var/log/meshtasticd.json
|
# TraceFile: /var/log/meshtasticd.json
|
||||||
# JSONFile: /packets.json # File location for JSON output of decoded packets
|
|
||||||
# JSONFilter: position # filter for packets to save to JSON file
|
|
||||||
# AsciiLogs: true # default if not specified is !isatty() on stdout
|
# AsciiLogs: true # default if not specified is !isatty() on stdout
|
||||||
|
|
||||||
Webserver:
|
Webserver:
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
Lora:
|
|
||||||
|
|
||||||
### RAK13300in Slot 1
|
|
||||||
Module: sx1262
|
|
||||||
IRQ: 22 #IO6
|
|
||||||
Reset: 16 # IO4
|
|
||||||
Busy: 24 # IO5
|
|
||||||
# Ant_sw: 13 # IO3
|
|
||||||
DIO3_TCXO_VOLTAGE: true
|
|
||||||
DIO2_AS_RF_SWITCH: true
|
|
||||||
spidev: spidev0.0
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
Lora:
|
|
||||||
Module: sx1262
|
|
||||||
CS: 0
|
|
||||||
IRQ: 6
|
|
||||||
Reset: 2
|
|
||||||
Busy: 4
|
|
||||||
RXen: 1
|
|
||||||
DIO2_AS_RF_SWITCH: true
|
|
||||||
spidev: ch341
|
|
||||||
DIO3_TCXO_VOLTAGE: true
|
|
||||||
# USB_Serialnum: 12345678
|
|
||||||
USB_PID: 0x5512
|
|
||||||
USB_VID: 0x1A86
|
|
||||||
SX126X_MAX_POWER: 22
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
Lora:
|
|
||||||
Module: sx1262
|
|
||||||
CS: 0
|
|
||||||
IRQ: 6
|
|
||||||
Reset: 1
|
|
||||||
Busy: 4
|
|
||||||
RXen: 2
|
|
||||||
DIO2_AS_RF_SWITCH: true
|
|
||||||
spidev: ch341
|
|
||||||
USB_PID: 0x5512
|
|
||||||
USB_VID: 0x1A86
|
|
||||||
DIO3_TCXO_VOLTAGE: true
|
|
||||||
# USB_Serialnum: 12345678
|
|
||||||
SX126X_MAX_POWER: 22
|
|
||||||
# Reduce output power to improve EMI
|
|
||||||
NUM_PA_POINTS: 22
|
|
||||||
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
|
|
||||||
# Note: This module integrates an additional PA to achieve higher output power.
|
|
||||||
# The 'power' parameter here does not represent the actual RF output.
|
|
||||||
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
|
||||||
# Each array element corresponds to the additional gain when that input level is set,
|
|
||||||
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
|
|
||||||
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
Lora:
|
|
||||||
Module: sx1268
|
|
||||||
CS: 0
|
|
||||||
IRQ: 6
|
|
||||||
Reset: 1
|
|
||||||
Busy: 4
|
|
||||||
RXen: 2
|
|
||||||
DIO2_AS_RF_SWITCH: true
|
|
||||||
spidev: ch341
|
|
||||||
USB_PID: 0x5512
|
|
||||||
USB_VID: 0x1A86
|
|
||||||
DIO3_TCXO_VOLTAGE: true
|
|
||||||
# USB_Serialnum: 12345678
|
|
||||||
SX126X_MAX_POWER: 22
|
|
||||||
# Reduce output power to improve EMI
|
|
||||||
NUM_PA_POINTS: 22
|
|
||||||
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
|
|
||||||
# Note: This module integrates an additional PA to achieve higher output power.
|
|
||||||
# The 'power' parameter here does not represent the actual RF output.
|
|
||||||
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
|
||||||
# Each array element corresponds to the additional gain when that input level is set,
|
|
||||||
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
|
|
||||||
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.
|
|
||||||
+10
-2
@@ -156,8 +156,16 @@ IF %BPS_RESET% EQU 1 (
|
|||||||
SET "PROGNAME=!FILENAME:.factory.bin=!"
|
SET "PROGNAME=!FILENAME:.factory.bin=!"
|
||||||
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
|
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
|
||||||
|
|
||||||
@REM Determine OTA filename based on MCU type (unified OTA format)
|
IF "__!MCU!__" == "__esp32s3__" (
|
||||||
SET "OTA_FILENAME=mt-!MCU!-ota.bin"
|
@REM We are working with ESP32-S3
|
||||||
|
SET "OTA_FILENAME=bleota-s3.bin"
|
||||||
|
) ELSE IF "__!MCU!__" == "__esp32c3__" (
|
||||||
|
@REM We are working with ESP32-C3
|
||||||
|
SET "OTA_FILENAME=bleota-c3.bin"
|
||||||
|
) ELSE (
|
||||||
|
@REM Everything else
|
||||||
|
SET "OTA_FILENAME=bleota.bin"
|
||||||
|
)
|
||||||
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
|
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
|
||||||
|
|
||||||
@REM Set SPIFFS filename with "littlefs-" prefix.
|
@REM Set SPIFFS filename with "littlefs-" prefix.
|
||||||
|
|||||||
+14
-21
@@ -32,19 +32,6 @@ if ! command -v jq >/dev/null 2>&1; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# esptool v5 supports commands with dashes and deprecates commands with
|
|
||||||
# underscores. Prior versions only support commands with underscores
|
|
||||||
if ${ESPTOOL_CMD} | grep --quiet write-flash
|
|
||||||
then
|
|
||||||
ESPTOOL_WRITE_FLASH=write-flash
|
|
||||||
ESPTOOL_ERASE_FLASH=erase-flash
|
|
||||||
ESPTOOL_READ_FLASH_STATUS=read-flash-status
|
|
||||||
else
|
|
||||||
ESPTOOL_WRITE_FLASH=write_flash
|
|
||||||
ESPTOOL_ERASE_FLASH=erase_flash
|
|
||||||
ESPTOOL_READ_FLASH_STATUS=read_flash_status
|
|
||||||
fi
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Usage info
|
# Usage info
|
||||||
@@ -96,8 +83,8 @@ while [ $# -gt 0 ]; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [[ $BPS_RESET == true ]]; then
|
if [[ $BPS_RESET == true ]]; then
|
||||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS}
|
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ -z "$FILENAME" ] && [ -n "$1" ] && {
|
[ -z "$FILENAME" ] && [ -n "$1" ] && {
|
||||||
@@ -131,8 +118,14 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Determine OTA filename based on MCU type (unified OTA format)
|
# Determine OTA filename based on MCU type
|
||||||
OTAFILE="mt-${MCU}-ota.bin"
|
if [ "$MCU" == "esp32s3" ]; then
|
||||||
|
OTAFILE=bleota-s3.bin
|
||||||
|
elif [ "$MCU" == "esp32c3" ]; then
|
||||||
|
OTAFILE=bleota-c3.bin
|
||||||
|
else
|
||||||
|
OTAFILE=bleota.bin
|
||||||
|
fi
|
||||||
|
|
||||||
# Set SPIFFS filename with "littlefs-" prefix.
|
# Set SPIFFS filename with "littlefs-" prefix.
|
||||||
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
|
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
|
||||||
@@ -151,12 +144,12 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
|
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
|
||||||
$ESPTOOL_CMD ${ESPTOOL_ERASE_FLASH}
|
$ESPTOOL_CMD erase-flash
|
||||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $FIRMWARE_OFFSET "${FILENAME}"
|
$ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}"
|
||||||
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
|
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
|
||||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OTA_OFFSET "${OTAFILE}"
|
$ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}"
|
||||||
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
|
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
|
||||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OFFSET "${SPIFFSFILE}"
|
$ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}"
|
||||||
|
|
||||||
else
|
else
|
||||||
show_help
|
show_help
|
||||||
|
|||||||
+2
-13
@@ -20,17 +20,6 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# esptool v5 supports commands with dashes and deprecates commands with
|
|
||||||
# underscores. Prior versions only support commands with underscores
|
|
||||||
if ${ESPTOOL_CMD} | grep --quiet write-flash
|
|
||||||
then
|
|
||||||
ESPTOOL_WRITE_FLASH=write-flash
|
|
||||||
ESPTOOL_READ_FLASH_STATUS=read-flash-status
|
|
||||||
else
|
|
||||||
ESPTOOL_WRITE_FLASH=write_flash
|
|
||||||
ESPTOOL_READ_FLASH_STATUS=read_flash_status
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Usage info
|
# Usage info
|
||||||
show_help() {
|
show_help() {
|
||||||
cat << EOF
|
cat << EOF
|
||||||
@@ -80,7 +69,7 @@ done
|
|||||||
shift "$((OPTIND-1))"
|
shift "$((OPTIND-1))"
|
||||||
|
|
||||||
if [ "$CHANGE_MODE" = true ]; then
|
if [ "$CHANGE_MODE" = true ]; then
|
||||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS}
|
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -91,7 +80,7 @@ fi
|
|||||||
|
|
||||||
if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then
|
if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then
|
||||||
echo "Trying to flash update ${FILENAME}"
|
echo "Trying to flash update ${FILENAME}"
|
||||||
$ESPTOOL_CMD --baud $FLASH_BAUD ${ESPTOOL_WRITE_FLASH} $UPDATE_OFFSET "${FILENAME}"
|
$ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}"
|
||||||
else
|
else
|
||||||
show_help
|
show_help
|
||||||
echo "Invalid file: ${FILENAME}"
|
echo "Invalid file: ${FILENAME}"
|
||||||
|
|||||||
@@ -1,355 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Generate release notes from merged PRs on develop and master branches.
|
|
||||||
Categorizes PRs into Enhancements and Bug Fixes/Maintenance sections.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
def get_last_release_tag():
|
|
||||||
"""Get the most recent release tag."""
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "describe", "--tags", "--abbrev=0"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
return result.stdout.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def get_tag_date(tag):
|
|
||||||
"""Get the commit date (ISO 8601) of the tag."""
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "show", "-s", "--format=%cI", tag],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
return result.stdout.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def get_merged_prs_since_tag(tag, branch):
|
|
||||||
"""Get all merged PRs since the given tag on the specified branch."""
|
|
||||||
# Get commits since tag on the branch - look for PR numbers in parentheses
|
|
||||||
result = subprocess.run(
|
|
||||||
[
|
|
||||||
"git",
|
|
||||||
"log",
|
|
||||||
f"{tag}..origin/{branch}",
|
|
||||||
"--oneline",
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
prs = []
|
|
||||||
seen_pr_numbers = set()
|
|
||||||
|
|
||||||
for line in result.stdout.strip().split("\n"):
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Extract PR number from commit message - format: "Title (#1234)"
|
|
||||||
pr_match = re.search(r"\(#(\d+)\)", line)
|
|
||||||
if pr_match:
|
|
||||||
pr_number = pr_match.group(1)
|
|
||||||
if pr_number not in seen_pr_numbers:
|
|
||||||
seen_pr_numbers.add(pr_number)
|
|
||||||
prs.append(pr_number)
|
|
||||||
|
|
||||||
return prs
|
|
||||||
|
|
||||||
|
|
||||||
def get_pr_details(pr_number):
|
|
||||||
"""Get PR details from GitHub API via gh CLI."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
[
|
|
||||||
"gh",
|
|
||||||
"pr",
|
|
||||||
"view",
|
|
||||||
pr_number,
|
|
||||||
"--json",
|
|
||||||
"title,author,labels,url",
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
return json.loads(result.stdout)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def should_exclude_pr(pr_details):
|
|
||||||
"""Check if PR should be excluded from release notes."""
|
|
||||||
if not pr_details:
|
|
||||||
return True
|
|
||||||
|
|
||||||
title = pr_details.get("title", "").lower()
|
|
||||||
|
|
||||||
# Exclude trunk update PRs
|
|
||||||
if "upgrade trunk" in title or "update trunk" in title or "trunk update" in title:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Exclude protobuf update PRs
|
|
||||||
if "update protobufs" in title or "update protobuf" in title:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Exclude automated version bump PRs
|
|
||||||
if "bump release version" in title or "bump version" in title:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def is_dependency_update(pr_details):
|
|
||||||
"""Check if PR is a dependency/chore update."""
|
|
||||||
if not pr_details:
|
|
||||||
return False
|
|
||||||
|
|
||||||
title = pr_details.get("title", "").lower()
|
|
||||||
author = pr_details.get("author", {}).get("login", "").lower()
|
|
||||||
labels = [label.get("name", "").lower() for label in pr_details.get("labels", [])]
|
|
||||||
|
|
||||||
# Check for renovate or dependabot authors
|
|
||||||
if "renovate" in author or "dependabot" in author:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check for chore(deps) pattern
|
|
||||||
if re.match(r"^chore\(deps\):", title):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check for digest update patterns
|
|
||||||
if re.match(r".*digest to [a-f0-9]+", title, re.IGNORECASE):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check for dependency-related labels
|
|
||||||
dependency_labels = ["dependencies", "deps", "renovate"]
|
|
||||||
if any(dep in label for label in labels for dep in dependency_labels):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def is_enhancement(pr_details):
|
|
||||||
"""Determine if PR is an enhancement based on labels and title."""
|
|
||||||
labels = [label.get("name", "").lower() for label in pr_details.get("labels", [])]
|
|
||||||
|
|
||||||
# Check labels first
|
|
||||||
enhancement_labels = ["enhancement", "feature", "feat", "new feature"]
|
|
||||||
for label in labels:
|
|
||||||
if any(enh in label for enh in enhancement_labels):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check title prefixes
|
|
||||||
title = pr_details.get("title", "")
|
|
||||||
enhancement_prefixes = ["feat:", "feature:", "add:"]
|
|
||||||
title_lower = title.lower()
|
|
||||||
for prefix in enhancement_prefixes:
|
|
||||||
if title_lower.startswith(prefix) or f" {prefix}" in title_lower:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def clean_title(title):
|
|
||||||
"""Clean up PR title for release notes."""
|
|
||||||
# Remove common prefixes
|
|
||||||
prefixes_to_remove = [
|
|
||||||
r"^fix:\s*",
|
|
||||||
r"^feat:\s*",
|
|
||||||
r"^feature:\s*",
|
|
||||||
r"^bug:\s*",
|
|
||||||
r"^bugfix:\s*",
|
|
||||||
r"^chore:\s*",
|
|
||||||
r"^chore\([^)]+\):\s*",
|
|
||||||
r"^refactor:\s*",
|
|
||||||
r"^docs:\s*",
|
|
||||||
r"^ci:\s*",
|
|
||||||
r"^build:\s*",
|
|
||||||
r"^perf:\s*",
|
|
||||||
r"^style:\s*",
|
|
||||||
r"^test:\s*",
|
|
||||||
]
|
|
||||||
|
|
||||||
cleaned = title
|
|
||||||
for prefix in prefixes_to_remove:
|
|
||||||
cleaned = re.sub(prefix, "", cleaned, flags=re.IGNORECASE)
|
|
||||||
|
|
||||||
# Ensure first letter is capitalized
|
|
||||||
if cleaned:
|
|
||||||
cleaned = cleaned[0].upper() + cleaned[1:]
|
|
||||||
|
|
||||||
return cleaned.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def format_pr_line(pr_details):
|
|
||||||
"""Format a PR as a markdown bullet point."""
|
|
||||||
title = clean_title(pr_details.get("title", "Unknown"))
|
|
||||||
author = pr_details.get("author", {}).get("login", "unknown")
|
|
||||||
url = pr_details.get("url", "")
|
|
||||||
|
|
||||||
return f"- {title} by @{author} in {url}"
|
|
||||||
|
|
||||||
|
|
||||||
def get_new_contributors(pr_details_list, tag, repo="meshtastic/firmware"):
|
|
||||||
"""Find contributors who made their first merged PR before this release.
|
|
||||||
|
|
||||||
GitHub usernames do not necessarily match git commit authors, so we use the
|
|
||||||
GitHub search API via `gh` to see if the user has any merged PRs before the
|
|
||||||
tag date. This mirrors how GitHub's "Generate release notes" feature works.
|
|
||||||
"""
|
|
||||||
|
|
||||||
bot_authors = {"github-actions", "renovate", "dependabot", "app/renovate", "app/github-actions", "app/dependabot"}
|
|
||||||
|
|
||||||
new_contributors = []
|
|
||||||
seen_authors = set()
|
|
||||||
|
|
||||||
try:
|
|
||||||
tag_date = get_tag_date(tag)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print(f"Warning: Could not determine tag date for {tag}; skipping new contributor detection", file=sys.stderr)
|
|
||||||
return []
|
|
||||||
|
|
||||||
for pr in pr_details_list:
|
|
||||||
author = pr.get("author", {}).get("login", "")
|
|
||||||
if not author or author in seen_authors:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip bots
|
|
||||||
if author.lower() in bot_authors or author.startswith("app/"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
seen_authors.add(author)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Search for merged PRs by this author created before the tag date
|
|
||||||
search_query = f"is:pr author:{author} repo:{repo} closed:<=\"{tag_date}\""
|
|
||||||
search = subprocess.run(
|
|
||||||
[
|
|
||||||
"gh",
|
|
||||||
"search",
|
|
||||||
"issues",
|
|
||||||
"--json",
|
|
||||||
"number,mergedAt,createdAt",
|
|
||||||
"--state",
|
|
||||||
"closed",
|
|
||||||
"--limit",
|
|
||||||
"200",
|
|
||||||
search_query,
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if search.returncode != 0:
|
|
||||||
# If gh fails, be conservative and skip adding to new contributors
|
|
||||||
print(f"Warning: gh search failed for author {author}: {search.stderr.strip()}", file=sys.stderr)
|
|
||||||
continue
|
|
||||||
|
|
||||||
results = json.loads(search.stdout or "[]")
|
|
||||||
# If any merged PR exists before or on tag date, not a new contributor
|
|
||||||
had_prior_pr = any(item.get("mergedAt") for item in results)
|
|
||||||
|
|
||||||
if not had_prior_pr:
|
|
||||||
new_contributors.append((author, pr.get("url", "")))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Could not check contributor history for {author}: {e}", file=sys.stderr)
|
|
||||||
continue
|
|
||||||
|
|
||||||
return new_contributors
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Usage: generate_release_notes.py <new_version>", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
new_version = sys.argv[1]
|
|
||||||
|
|
||||||
# Get last release tag
|
|
||||||
try:
|
|
||||||
last_tag = get_last_release_tag()
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print("Error: Could not find last release tag", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Collect PRs from both branches
|
|
||||||
all_pr_numbers = set()
|
|
||||||
|
|
||||||
for branch in ["develop", "master"]:
|
|
||||||
try:
|
|
||||||
prs = get_merged_prs_since_tag(last_tag, branch)
|
|
||||||
all_pr_numbers.update(prs)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Could not get PRs from {branch}: {e}", file=sys.stderr)
|
|
||||||
|
|
||||||
# Get details for all PRs
|
|
||||||
enhancements = []
|
|
||||||
bug_fixes = []
|
|
||||||
dependencies = []
|
|
||||||
all_pr_details = []
|
|
||||||
|
|
||||||
for pr_number in sorted(all_pr_numbers, key=int):
|
|
||||||
details = get_pr_details(pr_number)
|
|
||||||
if details and not should_exclude_pr(details):
|
|
||||||
all_pr_details.append(details)
|
|
||||||
if is_dependency_update(details):
|
|
||||||
dependencies.append(details)
|
|
||||||
elif is_enhancement(details):
|
|
||||||
enhancements.append(details)
|
|
||||||
else:
|
|
||||||
bug_fixes.append(details)
|
|
||||||
|
|
||||||
# Generate release notes
|
|
||||||
output = []
|
|
||||||
|
|
||||||
if enhancements:
|
|
||||||
output.append("## 🚀 Enhancements\n")
|
|
||||||
for pr in enhancements:
|
|
||||||
output.append(format_pr_line(pr))
|
|
||||||
output.append("")
|
|
||||||
|
|
||||||
if bug_fixes:
|
|
||||||
output.append("## 🐛 Bug fixes and maintenance\n")
|
|
||||||
for pr in bug_fixes:
|
|
||||||
output.append(format_pr_line(pr))
|
|
||||||
output.append("")
|
|
||||||
|
|
||||||
if dependencies:
|
|
||||||
output.append("## ⚙️ Dependencies\n")
|
|
||||||
for pr in dependencies:
|
|
||||||
output.append(format_pr_line(pr))
|
|
||||||
output.append("")
|
|
||||||
|
|
||||||
# Find new contributors (GitHub-accurate check using merged PRs before tag date)
|
|
||||||
new_contributors = get_new_contributors(all_pr_details, last_tag)
|
|
||||||
if new_contributors:
|
|
||||||
output.append("## New Contributors\n")
|
|
||||||
for author, url in new_contributors:
|
|
||||||
# Find first PR URL for this contributor
|
|
||||||
first_pr_url = url
|
|
||||||
for pr in all_pr_details:
|
|
||||||
if pr.get("author", {}).get("login") == author:
|
|
||||||
first_pr_url = pr.get("url", url)
|
|
||||||
break
|
|
||||||
output.append(f"- @{author} made their first contribution in {first_pr_url}")
|
|
||||||
output.append("")
|
|
||||||
|
|
||||||
# Add full changelog link
|
|
||||||
output.append(
|
|
||||||
f"**Full Changelog**: https://github.com/meshtastic/firmware/compare/{last_tag}...v{new_version}"
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\n".join(output))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
INSTANCE=$1
|
|
||||||
CONF_DIR="/etc/meshtasticd/config.d"
|
|
||||||
VFS_DIR="/var/lib"
|
|
||||||
|
|
||||||
# If no instance ID provided, start bare daemon and exit
|
|
||||||
echo "no instance ID provided, starting bare meshtasticd service"
|
|
||||||
if [ -z "${INSTANCE}" ]; then
|
|
||||||
/usr/bin/meshtasticd
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Make VFS dir if it does not exist
|
|
||||||
if [ ! -d "${VFS_DIR}/meshtasticd-${INSTANCE}" ]; then
|
|
||||||
echo "vfs for ${INSTANCE} does not exist, creating it."
|
|
||||||
mkdir "${VFS_DIR}/meshtasticd-${INSTANCE}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Abort if config for $INSTANCE does not exist
|
|
||||||
if [ ! -f "${CONF_DIR}/config-${INSTANCE}.yaml" ]; then
|
|
||||||
echo "no config for ${INSTANCE} found in ${CONF_DIR}. refusing to start" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Start meshtasticd with instance parameters
|
|
||||||
printf "starting meshtasticd-%s..., ${INSTANCE}"
|
|
||||||
if /usr/bin/meshtasticd --config="${CONF_DIR}/config-${INSTANCE}.yaml" --fsdir="${VFS_DIR}/meshtasticd-${INSTANCE}"; then
|
|
||||||
echo "ok"
|
|
||||||
else
|
|
||||||
echo "failed"
|
|
||||||
fi
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Meshtastic %i Daemon
|
Description=Meshtastic Native Daemon
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
StartLimitInterval=200
|
StartLimitInterval=200
|
||||||
StartLimitBurst=5
|
StartLimitBurst=5
|
||||||
@@ -9,7 +9,7 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|||||||
User=meshtasticd
|
User=meshtasticd
|
||||||
Group=meshtasticd
|
Group=meshtasticd
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/bin/meshtasticd-start.sh %i
|
ExecStart=/usr/bin/meshtasticd
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
cp "release/meshtasticd_linux_$(uname -m)" /usr/bin/meshtasticd
|
cp "release/meshtasticd_linux_$(uname -m)" /usr/bin/meshtasticd
|
||||||
cp "bin/meshtasticd-start.sh" /usr/bin/meshtasticd-start.sh
|
|
||||||
mkdir -p /etc/meshtasticd
|
mkdir -p /etc/meshtasticd
|
||||||
if [[ -f "/etc/meshtasticd/config.yaml" ]]; then
|
if [[ -f "/etc/meshtasticd/config.yaml" ]]; then
|
||||||
cp bin/config-dist.yaml /etc/meshtasticd/config-upgrade.yaml
|
cp bin/config-dist.yaml /etc/meshtasticd/config-upgrade.yaml
|
||||||
|
|||||||
@@ -87,12 +87,6 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
<release version="2.7.19" date="2026-01-22">
|
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19</url>
|
|
||||||
</release>
|
|
||||||
<release version="2.7.18" date="2026-01-02">
|
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18</url>
|
|
||||||
</release>
|
|
||||||
<release version="2.7.17" date="2025-11-28">
|
<release version="2.7.17" date="2025-11-28">
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17</url>
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17</url>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
+20
-171
@@ -2,12 +2,11 @@
|
|||||||
# trunk-ignore-all(ruff/F821)
|
# trunk-ignore-all(ruff/F821)
|
||||||
# trunk-ignore-all(flake8/F821): For SConstruct imports
|
# trunk-ignore-all(flake8/F821): For SConstruct imports
|
||||||
import sys
|
import sys
|
||||||
from os.path import join
|
from os.path import join, basename, isfile
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from readprops import readProps
|
from readprops import readProps
|
||||||
|
|
||||||
@@ -15,59 +14,9 @@ Import("env")
|
|||||||
platform = env.PioPlatform()
|
platform = env.PioPlatform()
|
||||||
progname = env.get("PROGNAME")
|
progname = env.get("PROGNAME")
|
||||||
lfsbin = f"{progname.replace('firmware-', 'littlefs-')}.bin"
|
lfsbin = f"{progname.replace('firmware-', 'littlefs-')}.bin"
|
||||||
manifest_ran = False
|
|
||||||
|
|
||||||
def infer_architecture(board_cfg):
|
|
||||||
try:
|
|
||||||
mcu = board_cfg.get("build.mcu") if board_cfg else None
|
|
||||||
except KeyError:
|
|
||||||
mcu = None
|
|
||||||
except Exception:
|
|
||||||
mcu = None
|
|
||||||
if not mcu:
|
|
||||||
return None
|
|
||||||
mcu_l = str(mcu).lower()
|
|
||||||
if "esp32s3" in mcu_l:
|
|
||||||
return "esp32-s3"
|
|
||||||
if "esp32c6" in mcu_l:
|
|
||||||
return "esp32-c6"
|
|
||||||
if "esp32c3" in mcu_l:
|
|
||||||
return "esp32-c3"
|
|
||||||
if "esp32" in mcu_l:
|
|
||||||
return "esp32"
|
|
||||||
if "rp2040" in mcu_l:
|
|
||||||
return "rp2040"
|
|
||||||
if "rp2350" in mcu_l:
|
|
||||||
return "rp2350"
|
|
||||||
if "nrf52" in mcu_l or "nrf52840" in mcu_l:
|
|
||||||
return "nrf52840"
|
|
||||||
if "stm32" in mcu_l:
|
|
||||||
return "stm32"
|
|
||||||
return None
|
|
||||||
|
|
||||||
def manifest_gather(source, target, env):
|
def manifest_gather(source, target, env):
|
||||||
global manifest_ran
|
|
||||||
if manifest_ran:
|
|
||||||
return
|
|
||||||
# Skip manifest generation if we cannot determine architecture (host/native builds)
|
|
||||||
board_arch = infer_architecture(env.BoardConfig())
|
|
||||||
if not board_arch:
|
|
||||||
print(f"Skipping mtjson generation for unknown architecture (env={env.get('PIOENV')})")
|
|
||||||
manifest_ran = True
|
|
||||||
return
|
|
||||||
manifest_ran = True
|
|
||||||
out = []
|
out = []
|
||||||
board_platform = env.BoardConfig().get("platform")
|
|
||||||
board_mcu = env.BoardConfig().get("build.mcu").lower()
|
|
||||||
needs_ota_suffix = board_platform == "nordicnrf52"
|
|
||||||
|
|
||||||
# Mapping of bin files to their target partition names
|
|
||||||
# Maps the filename pattern to the partition name where it should be flashed
|
|
||||||
partition_map = {
|
|
||||||
f"{progname}.bin": "app0", # primary application slot (app0 / OTA_0)
|
|
||||||
lfsbin: "spiffs", # filesystem image flashed to spiffs
|
|
||||||
}
|
|
||||||
|
|
||||||
check_paths = [
|
check_paths = [
|
||||||
progname,
|
progname,
|
||||||
f"{progname}.elf",
|
f"{progname}.elf",
|
||||||
@@ -78,62 +27,29 @@ def manifest_gather(source, target, env):
|
|||||||
f"{progname}.uf2",
|
f"{progname}.uf2",
|
||||||
f"{progname}.factory.uf2",
|
f"{progname}.factory.uf2",
|
||||||
f"{progname}.zip",
|
f"{progname}.zip",
|
||||||
lfsbin,
|
lfsbin
|
||||||
f"mt-{board_mcu}-ota.bin",
|
|
||||||
"bleota-c3.bin"
|
|
||||||
]
|
]
|
||||||
for p in check_paths:
|
for p in check_paths:
|
||||||
f = env.File(env.subst(f"$BUILD_DIR/{p}"))
|
f = env.File(env.subst(f"$BUILD_DIR/{p}"))
|
||||||
if f.exists():
|
if f.exists():
|
||||||
manifest_name = p
|
|
||||||
if needs_ota_suffix and p == f"{progname}.zip":
|
|
||||||
manifest_name = f"{progname}-ota.zip"
|
|
||||||
d = {
|
d = {
|
||||||
"name": manifest_name,
|
"name": p,
|
||||||
"md5": f.get_content_hash(), # Returns MD5 hash
|
"md5": f.get_content_hash(), # Returns MD5 hash
|
||||||
"bytes": f.get_size() # Returns file size in bytes
|
"bytes": f.get_size() # Returns file size in bytes
|
||||||
}
|
}
|
||||||
# Add part_name if this file represents a partition that should be flashed
|
|
||||||
if p in partition_map:
|
|
||||||
d["part_name"] = partition_map[p]
|
|
||||||
out.append(d)
|
out.append(d)
|
||||||
print(d)
|
print(d)
|
||||||
manifest_write(out, env)
|
manifest_write(out, env)
|
||||||
|
|
||||||
def manifest_write(files, env):
|
def manifest_write(files, env):
|
||||||
# Defensive: also skip manifest writing if we cannot determine architecture
|
|
||||||
def get_project_option(name):
|
|
||||||
try:
|
|
||||||
return env.GetProjectOption(name)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_project_option_any(names):
|
|
||||||
for name in names:
|
|
||||||
val = get_project_option(name)
|
|
||||||
if val is not None:
|
|
||||||
return val
|
|
||||||
return None
|
|
||||||
|
|
||||||
def as_bool(val):
|
|
||||||
return str(val).strip().lower() in ("1", "true", "yes", "on")
|
|
||||||
|
|
||||||
def as_int(val):
|
|
||||||
try:
|
|
||||||
return int(str(val), 10)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def as_list(val):
|
|
||||||
return [item.strip() for item in str(val).split(",") if item.strip()]
|
|
||||||
|
|
||||||
manifest = {
|
manifest = {
|
||||||
"version": verObj["long"],
|
"version": verObj["long"],
|
||||||
"build_epoch": build_epoch,
|
"build_epoch": build_epoch,
|
||||||
"platformioTarget": env.get("PIOENV"),
|
"board": env.get("PIOENV"),
|
||||||
"mcu": env.get("BOARD_MCU"),
|
"mcu": env.get("BOARD_MCU"),
|
||||||
"repo": repo_owner,
|
"repo": repo_owner,
|
||||||
"files": files,
|
"files": files,
|
||||||
|
"part": None,
|
||||||
"has_mui": False,
|
"has_mui": False,
|
||||||
"has_inkhud": False,
|
"has_inkhud": False,
|
||||||
}
|
}
|
||||||
@@ -148,51 +64,6 @@ def manifest_write(files, env):
|
|||||||
if "MESHTASTIC_INCLUDE_INKHUD" in env.get("CPPDEFINES", []):
|
if "MESHTASTIC_INCLUDE_INKHUD" in env.get("CPPDEFINES", []):
|
||||||
manifest["has_inkhud"] = True
|
manifest["has_inkhud"] = True
|
||||||
|
|
||||||
pioenv = env.get("PIOENV")
|
|
||||||
device_meta = {}
|
|
||||||
device_meta_fields = [
|
|
||||||
("hwModel", ["custom_meshtastic_hw_model"], as_int),
|
|
||||||
("hwModelSlug", ["custom_meshtastic_hw_model_slug"], str),
|
|
||||||
("architecture", ["custom_meshtastic_architecture"], str),
|
|
||||||
("activelySupported", ["custom_meshtastic_actively_supported"], as_bool),
|
|
||||||
("displayName", ["custom_meshtastic_display_name"], str),
|
|
||||||
("supportLevel", ["custom_meshtastic_support_level"], as_int),
|
|
||||||
("images", ["custom_meshtastic_images"], as_list),
|
|
||||||
("tags", ["custom_meshtastic_tags"], as_list),
|
|
||||||
("requiresDfu", ["custom_meshtastic_requires_dfu"], as_bool),
|
|
||||||
("partitionScheme", ["custom_meshtastic_partition_scheme"], str),
|
|
||||||
("url", ["custom_meshtastic_url"], str),
|
|
||||||
("key", ["custom_meshtastic_key"], str),
|
|
||||||
("variant", ["custom_meshtastic_variant"], str),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
for manifest_key, option_keys, caster in device_meta_fields:
|
|
||||||
raw_val = get_project_option_any(option_keys)
|
|
||||||
if raw_val is None:
|
|
||||||
continue
|
|
||||||
parsed = caster(raw_val) if callable(caster) else raw_val
|
|
||||||
if parsed is not None and parsed != "":
|
|
||||||
device_meta[manifest_key] = parsed
|
|
||||||
|
|
||||||
# Determine architecture once; if we can't infer it, skip manifest generation
|
|
||||||
board_arch = device_meta.get("architecture") or infer_architecture(env.BoardConfig())
|
|
||||||
if not board_arch:
|
|
||||||
print(f"Skipping mtjson write for unknown architecture (env={env.get('PIOENV')})")
|
|
||||||
return
|
|
||||||
|
|
||||||
device_meta["architecture"] = board_arch
|
|
||||||
|
|
||||||
# Always set requiresDfu: true for nrf52840 targets
|
|
||||||
if board_arch == "nrf52840":
|
|
||||||
device_meta["requiresDfu"] = True
|
|
||||||
|
|
||||||
device_meta.setdefault("displayName", pioenv)
|
|
||||||
device_meta.setdefault("activelySupported", False)
|
|
||||||
|
|
||||||
if device_meta:
|
|
||||||
manifest.update(device_meta)
|
|
||||||
|
|
||||||
# Write the manifest to the build directory
|
# Write the manifest to the build directory
|
||||||
with open(env.subst("$BUILD_DIR/${PROGNAME}.mt.json"), "w") as f:
|
with open(env.subst("$BUILD_DIR/${PROGNAME}.mt.json"), "w") as f:
|
||||||
json.dump(manifest, f, indent=2)
|
json.dump(manifest, f, indent=2)
|
||||||
@@ -288,42 +159,20 @@ def load_boot_logo(source, target, env):
|
|||||||
|
|
||||||
# Load the boot logo on TFT builds
|
# Load the boot logo on TFT builds
|
||||||
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
|
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
|
||||||
env.AddPreAction(f"$BUILD_DIR/{lfsbin}", load_boot_logo)
|
env.AddPreAction('$BUILD_DIR/littlefs.bin', load_boot_logo)
|
||||||
|
|
||||||
board_arch = infer_architecture(env.BoardConfig())
|
# Rename (mv) littlefs.bin to include the PROGNAME
|
||||||
should_skip_manifest = board_arch is None
|
# This ensures the littlefs.bin is named consistently with the firmware
|
||||||
|
env.AddPostAction('$BUILD_DIR/littlefs.bin', env.VerboseAction(
|
||||||
|
f'mv $BUILD_DIR/littlefs.bin $BUILD_DIR/{lfsbin}',
|
||||||
|
f'Renaming littlefs.bin to {lfsbin}'
|
||||||
|
))
|
||||||
|
|
||||||
# For host/native envs, avoid depending on 'buildprog' (some targets don't define it)
|
env.AddCustomTarget(
|
||||||
mtjson_deps = [] if should_skip_manifest else ["buildprog"]
|
name="mtjson",
|
||||||
if not should_skip_manifest and platform.name == "espressif32":
|
dependencies=None,
|
||||||
# Build littlefs image as part of mtjson target
|
actions=[manifest_gather],
|
||||||
# Equivalent to `pio run -t buildfs`
|
title="Meshtastic Manifest",
|
||||||
target_lfs = env.DataToBin(
|
description="Generating Meshtastic manifest JSON + Checksums",
|
||||||
join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR"
|
always_build=True,
|
||||||
)
|
)
|
||||||
mtjson_deps.append(target_lfs)
|
|
||||||
|
|
||||||
if should_skip_manifest:
|
|
||||||
def skip_manifest(source, target, env):
|
|
||||||
print(f"mtjson: skipped for native environment: {env.get('PIOENV')}")
|
|
||||||
|
|
||||||
env.AddCustomTarget(
|
|
||||||
name="mtjson",
|
|
||||||
dependencies=mtjson_deps,
|
|
||||||
actions=[skip_manifest],
|
|
||||||
title="Meshtastic Manifest (skipped)",
|
|
||||||
description="mtjson generation is skipped for native environments",
|
|
||||||
always_build=True,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
env.AddCustomTarget(
|
|
||||||
name="mtjson",
|
|
||||||
dependencies=mtjson_deps,
|
|
||||||
actions=[manifest_gather],
|
|
||||||
title="Meshtastic Manifest",
|
|
||||||
description="Generating Meshtastic manifest JSON + Checksums",
|
|
||||||
always_build=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Run manifest generation as part of the default build pipeline for non-native builds.
|
|
||||||
env.Default("mtjson")
|
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ else:
|
|||||||
prefsLoc = env["PROJECT_DIR"] + "/version.properties"
|
prefsLoc = env["PROJECT_DIR"] + "/version.properties"
|
||||||
verObj = readProps(prefsLoc)
|
verObj = readProps(prefsLoc)
|
||||||
env.Replace(PROGNAME=f"firmware-{env.get('PIOENV')}-{verObj['long']}")
|
env.Replace(PROGNAME=f"firmware-{env.get('PIOENV')}-{verObj['long']}")
|
||||||
env.Replace(ESP32_FS_IMAGE_NAME=f"littlefs-{env.get('PIOENV')}-{verObj['long']}")
|
|
||||||
|
|
||||||
# Print the new program name for verification
|
# Print the new program name for verification
|
||||||
print(f"PROGNAME: {env.get('PROGNAME')}")
|
print(f"PROGNAME: {env.get('PROGNAME')}")
|
||||||
if platform.name == "espressif32":
|
|
||||||
print(f"ESP32_FS_IMAGE_NAME: {env.get('ESP32_FS_IMAGE_NAME')}")
|
|
||||||
|
|||||||
+1
-2
@@ -18,9 +18,8 @@ def readProps(prefsLoc):
|
|||||||
|
|
||||||
# Try to find current build SHA if if the workspace is clean. This could fail if git is not installed
|
# Try to find current build SHA if if the workspace is clean. This could fail if git is not installed
|
||||||
try:
|
try:
|
||||||
# Pin abbreviation length to keep local builds and CI matching (avoid auto-shortening)
|
|
||||||
sha = (
|
sha = (
|
||||||
subprocess.check_output(["git", "rev-parse", "--short=7", "HEAD"])
|
subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
|
||||||
.decode("utf-8")
|
.decode("utf-8")
|
||||||
.strip()
|
.strip()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from github import Github
|
|
||||||
|
|
||||||
def parseFile(path):
|
|
||||||
with open(path, "r") as f:
|
|
||||||
data = json.loads(f)
|
|
||||||
for file in data["files"]:
|
|
||||||
if file["name"].endswith(".bin"):
|
|
||||||
return file["name"], file["bytes"]
|
|
||||||
|
|
||||||
if len(sys.argv) != 4:
|
|
||||||
print(f"expected usage: {sys.argv[0]} <PR number> <path to old-manifests> <path to new-manifests>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
pr_number = int(sys.argv[1])
|
|
||||||
|
|
||||||
token = os.getenv("GITHUB_TOKEN")
|
|
||||||
if not token:
|
|
||||||
raise EnvironmentError("GITHUB_TOKEN not found in environment.")
|
|
||||||
|
|
||||||
repo_name = os.getenv("GITHUB_REPOSITORY") # "owner/repo"
|
|
||||||
if not repo_name:
|
|
||||||
raise EnvironmentError("GITHUB_REPOSITORY not found in environment.")
|
|
||||||
|
|
||||||
oldFiles = sys.argv[2]
|
|
||||||
old = set(os.path.join(oldFiles, f) for f in os.listdir(oldFiles) if os.path.isfile(f))
|
|
||||||
newFiles = sys.argv[3]
|
|
||||||
new = set(os.path.join(newFiles, f) for f in os.listdir(newFiles) if os.path.isfile(f))
|
|
||||||
|
|
||||||
startMarkdown = "# Target Size Changes\n\n"
|
|
||||||
markdown = ""
|
|
||||||
|
|
||||||
newlyIntroduced = new - old
|
|
||||||
if len(newlyIntroduced) > 0:
|
|
||||||
markdown += "## Newly Introduced Targets\n\n"
|
|
||||||
# create a table
|
|
||||||
markdown += "| File | Size |\n"
|
|
||||||
markdown += "| ---- | ---- |\n"
|
|
||||||
for f in newlyIntroduced:
|
|
||||||
name, size = parseFile(f)
|
|
||||||
markdown += f"| `{name}` | {size}b |\n"
|
|
||||||
|
|
||||||
# do not log removed targets
|
|
||||||
# PRs only run a small subset of builds, so removed targets are not meaningful
|
|
||||||
# since they are very likely to just be not ran in PR CI
|
|
||||||
|
|
||||||
both = old & new
|
|
||||||
degradations = []
|
|
||||||
improvements = []
|
|
||||||
for f in both:
|
|
||||||
oldName, oldSize = parseFile(f)
|
|
||||||
_, newSize = parseFile(f)
|
|
||||||
if oldSize != newSize:
|
|
||||||
if newSize < oldSize:
|
|
||||||
improvements.append((oldName, oldSize, newSize))
|
|
||||||
else:
|
|
||||||
degradations.append((oldName, oldSize, newSize))
|
|
||||||
|
|
||||||
if len(degradations) > 0:
|
|
||||||
markdown += "\n## Degradation\n\n"
|
|
||||||
# create a table
|
|
||||||
markdown += "| File | Difference | Old Size | New Size |\n"
|
|
||||||
markdown += "| ---- | ---------- | -------- | -------- |\n"
|
|
||||||
for oldName, oldSize, newSize in degradations:
|
|
||||||
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
|
|
||||||
|
|
||||||
if len(improvements) > 0:
|
|
||||||
markdown += "\n## Improvement\n\n"
|
|
||||||
# create a table
|
|
||||||
markdown += "| File | Difference | Old Size | New Size |\n"
|
|
||||||
markdown += "| ---- | ---------- | -------- | -------- |\n"
|
|
||||||
for oldName, oldSize, newSize in improvements:
|
|
||||||
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
|
|
||||||
|
|
||||||
if len(markdown) == 0:
|
|
||||||
markdown = "No changes in target sizes detected."
|
|
||||||
|
|
||||||
g = Github(token)
|
|
||||||
repo = g.get_repo(repo_name)
|
|
||||||
pr = repo.get_pull(pr_number)
|
|
||||||
|
|
||||||
existing_comment = None
|
|
||||||
for comment in pr.get_issue_comments():
|
|
||||||
if comment.body.startswith(startMarkdown):
|
|
||||||
existing_comment = comment
|
|
||||||
break
|
|
||||||
|
|
||||||
final_markdown = startMarkdown + markdown
|
|
||||||
|
|
||||||
if existing_comment:
|
|
||||||
existing_comment.edit(body=final_markdown)
|
|
||||||
else:
|
|
||||||
pr.create_issue_comment(body=final_markdown)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"build": {
|
|
||||||
"arduino": {
|
|
||||||
"ldscript": "esp32s3_out.ld",
|
|
||||||
"partitions": "default.csv",
|
|
||||||
"memory_type": "qio_qspi"
|
|
||||||
},
|
|
||||||
"core": "esp32",
|
|
||||||
"extra_flags": [
|
|
||||||
"-DBOARD_HAS_PSRAM",
|
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=0",
|
|
||||||
"-DARDUINO_RUNNING_CORE=1",
|
|
||||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
|
||||||
],
|
|
||||||
"f_cpu": "240000000L",
|
|
||||||
"f_flash": "80000000L",
|
|
||||||
"flash_mode": "qio",
|
|
||||||
"mcu": "esp32s3",
|
|
||||||
"variant": "esp32s3"
|
|
||||||
},
|
|
||||||
"connectivity": ["wifi"],
|
|
||||||
"debug": {
|
|
||||||
"openocd_target": "esp32s3.cfg"
|
|
||||||
},
|
|
||||||
"frameworks": ["arduino", "espidf"],
|
|
||||||
"name": "CDEBYTE_EoRa-Hub",
|
|
||||||
"upload": {
|
|
||||||
"flash_size": "4MB",
|
|
||||||
"maximum_ram_size": 327680,
|
|
||||||
"maximum_size": 4194304,
|
|
||||||
"use_1200bps_touch": true,
|
|
||||||
"wait_for_upload_port": true,
|
|
||||||
"require_upload_port": true,
|
|
||||||
"speed": 921600
|
|
||||||
},
|
|
||||||
"url": "https://www.cdebyte.com/products/EoRa-HUB-900TB",
|
|
||||||
"vendor": "CDEBYTE"
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"build": {
|
|
||||||
"arduino": {
|
|
||||||
"ldscript": "nrf52840_s140_v6.ld"
|
|
||||||
},
|
|
||||||
"core": "nRF5",
|
|
||||||
"cpu": "cortex-m4",
|
|
||||||
"extra_flags": "-DARDUINO_NRF52840_ELECROW_M4 -DNRF52840_XXAA",
|
|
||||||
"f_cpu": "64000000L",
|
|
||||||
"hwids": [
|
|
||||||
["0x239A", "0x4405"],
|
|
||||||
["0x239A", "0x0029"],
|
|
||||||
["0x239A", "0x002A"]
|
|
||||||
],
|
|
||||||
"usb_product": "elecrow_thinknode_m4",
|
|
||||||
"mcu": "nrf52840",
|
|
||||||
"variant": "ELECROW-ThinkNode-M4",
|
|
||||||
"variants_dir": "variants",
|
|
||||||
"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",
|
|
||||||
"onboard_tools": ["jlink"],
|
|
||||||
"svd_path": "nrf52840.svd",
|
|
||||||
"openocd_target": "nrf52840-mdk-rs"
|
|
||||||
},
|
|
||||||
"frameworks": ["arduino"],
|
|
||||||
"name": "ELECROW ThinkNode m4",
|
|
||||||
"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://www.elecrow.com/thinknode-m4-power-bank-lora-device-with-meshtastic-lora-tracker-function-powered-by-nrf52840.html",
|
|
||||||
"vendor": "ELECROW"
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"build": {
|
|
||||||
"arduino": {
|
|
||||||
"ldscript": "nrf52840_s140_v6.ld"
|
|
||||||
},
|
|
||||||
"core": "nRF5",
|
|
||||||
"cpu": "cortex-m4",
|
|
||||||
"extra_flags": "-DMINIMESH_LITE -DNRF52840_XXAA",
|
|
||||||
"f_cpu": "64000000L",
|
|
||||||
"hwids": [
|
|
||||||
["0x239A", "0x8029"],
|
|
||||||
["0x239A", "0x0029"],
|
|
||||||
["0x239A", "0x002A"]
|
|
||||||
],
|
|
||||||
"usb_product": "Minimesh Lite",
|
|
||||||
"mcu": "nrf52840",
|
|
||||||
"variant": "dls_Minimesh_Lite",
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"frameworks": ["arduino"],
|
|
||||||
"name": "Minimesh Lite",
|
|
||||||
"upload": {
|
|
||||||
"maximum_ram_size": 248832,
|
|
||||||
"maximum_size": 815104,
|
|
||||||
"speed": 115200,
|
|
||||||
"protocol": "nrfutil",
|
|
||||||
"protocols": ["nrfutil", "jlink", "nrfjprog", "stlink"],
|
|
||||||
"use_1200bps_touch": true,
|
|
||||||
"require_upload_port": true,
|
|
||||||
"wait_for_upload_port": true
|
|
||||||
},
|
|
||||||
"url": "https://deeplabstudio.com",
|
|
||||||
"vendor": "Deeplab Studio"
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"build": {
|
|
||||||
"arduino": {
|
|
||||||
"ldscript": "esp32s3_out.ld",
|
|
||||||
"memory_type": "qio_opi"
|
|
||||||
},
|
|
||||||
"core": "esp32",
|
|
||||||
"extra_flags": [
|
|
||||||
"-DBOARD_HAS_PSRAM",
|
|
||||||
"-DLILYGO_TBEAM_1W",
|
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
|
||||||
"-DARDUINO_USB_MODE=0",
|
|
||||||
"-DARDUINO_RUNNING_CORE=1",
|
|
||||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
|
||||||
],
|
|
||||||
"f_cpu": "240000000L",
|
|
||||||
"f_flash": "80000000L",
|
|
||||||
"flash_mode": "qio",
|
|
||||||
"psram_type": "opi",
|
|
||||||
"hwids": [["0x303A", "0x1001"]],
|
|
||||||
"mcu": "esp32s3",
|
|
||||||
"variant": "t-beam-1w"
|
|
||||||
},
|
|
||||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
|
||||||
"debug": {
|
|
||||||
"openocd_target": "esp32s3.cfg"
|
|
||||||
},
|
|
||||||
"frameworks": ["arduino"],
|
|
||||||
"name": "LilyGo TBeam-1W",
|
|
||||||
"upload": {
|
|
||||||
"flash_size": "16MB",
|
|
||||||
"maximum_ram_size": 327680,
|
|
||||||
"maximum_size": 16777216,
|
|
||||||
"require_upload_port": true,
|
|
||||||
"speed": 921600
|
|
||||||
},
|
|
||||||
"url": "http://www.lilygo.cn/",
|
|
||||||
"vendor": "LilyGo"
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"-DBOARD_HAS_PSRAM",
|
"-DBOARD_HAS_PSRAM",
|
||||||
"-DT_WATCH_S3",
|
"-DT_WATCH_S3",
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||||
"-DARDUINO_USB_MODE=1",
|
"-DARDUINO_USB_MODE=0",
|
||||||
"-DARDUINO_RUNNING_CORE=1",
|
"-DARDUINO_RUNNING_CORE=1",
|
||||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||||
],
|
],
|
||||||
|
|||||||
Vendored
-12
@@ -1,15 +1,3 @@
|
|||||||
meshtasticd (2.7.19.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Version 2.7.19
|
|
||||||
|
|
||||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Thu, 22 Jan 2026 22:17:40 +0000
|
|
||||||
|
|
||||||
meshtasticd (2.7.18.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Version 2.7.18
|
|
||||||
|
|
||||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Fri, 02 Jan 2026 12:45:36 +0000
|
|
||||||
|
|
||||||
meshtasticd (2.7.17.0) unstable; urgency=medium
|
meshtasticd (2.7.17.0) unstable; urgency=medium
|
||||||
|
|
||||||
* Version 2.7.17
|
* Version 2.7.17
|
||||||
|
|||||||
Vendored
+1
-2
@@ -25,8 +25,7 @@ Build-Depends: debhelper-compat (= 13),
|
|||||||
liborcania-dev,
|
liborcania-dev,
|
||||||
libx11-dev,
|
libx11-dev,
|
||||||
libinput-dev,
|
libinput-dev,
|
||||||
libxkbcommon-x11-dev,
|
libxkbcommon-x11-dev
|
||||||
libsqlite3-dev
|
|
||||||
Standards-Version: 4.6.2
|
Standards-Version: 4.6.2
|
||||||
Homepage: https://github.com/meshtastic/firmware
|
Homepage: https://github.com/meshtastic/firmware
|
||||||
Rules-Requires-Root: no
|
Rules-Requires-Root: no
|
||||||
|
|||||||
Vendored
-1
@@ -4,6 +4,5 @@ bin/config.yaml etc/meshtasticd
|
|||||||
bin/config.d/* etc/meshtasticd/available.d
|
bin/config.d/* etc/meshtasticd/available.d
|
||||||
|
|
||||||
bin/meshtasticd.service lib/systemd/system
|
bin/meshtasticd.service lib/systemd/system
|
||||||
bin/meshtasticd-start.sh usr/bin
|
|
||||||
|
|
||||||
web/* usr/share/meshtasticd/web
|
web/* usr/share/meshtasticd/web
|
||||||
|
|||||||
@@ -10,12 +10,6 @@ Import("env")
|
|||||||
platform = env.PioPlatform()
|
platform = env.PioPlatform()
|
||||||
|
|
||||||
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
|
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
|
||||||
# IntelHex workaround, remove after fixed upstream
|
|
||||||
# https://github.com/platformio/platform-espressif32/issues/1632
|
|
||||||
try:
|
|
||||||
import intelhex
|
|
||||||
except ImportError:
|
|
||||||
env.Execute("$PYTHONEXE -m pip install intelhex")
|
|
||||||
import esptool
|
import esptool
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Generated
-44
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-compat": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1767039857,
|
|
||||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1766314097,
|
|
||||||
"narHash": "sha256-laJftWbghBehazn/zxVJ8NdENVgjccsWAdAqKXhErrM=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "306ea70f9eb0fb4e040f8540e2deab32ed7e2055",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-compat": "flake-compat",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
{
|
|
||||||
description = "Nix flake to compile Meshtastic firmware";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
|
||||||
|
|
||||||
# Shim to make flake.nix work with stable Nix.
|
|
||||||
flake-compat = {
|
|
||||||
url = "github:NixOS/flake-compat";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
inputs:
|
|
||||||
let
|
|
||||||
lib = inputs.nixpkgs.lib;
|
|
||||||
|
|
||||||
forAllSystems =
|
|
||||||
fn:
|
|
||||||
lib.genAttrs lib.systems.flakeExposed (
|
|
||||||
system:
|
|
||||||
fn {
|
|
||||||
pkgs = import inputs.nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
};
|
|
||||||
inherit system;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShells = forAllSystems (
|
|
||||||
{ pkgs, ... }:
|
|
||||||
let
|
|
||||||
python3 = pkgs.python312.withPackages (
|
|
||||||
ps: with ps; [
|
|
||||||
google
|
|
||||||
]
|
|
||||||
);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
default = pkgs.mkShell {
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
python3
|
|
||||||
platformio
|
|
||||||
];
|
|
||||||
|
|
||||||
shellHook = ''
|
|
||||||
# Set up PlatformIO to use a local core directory.
|
|
||||||
export PLATFORMIO_CORE_DIR=$PWD/.platformio
|
|
||||||
# Tell pip to put packages into $PIP_PREFIX instead of the usual
|
|
||||||
# location. This is especially necessary under NixOS to avoid having
|
|
||||||
# pip trying to write to the read-only Nix store. For more info,
|
|
||||||
# see https://wiki.nixos.org/wiki/Python
|
|
||||||
export PIP_PREFIX=$PWD/.python3
|
|
||||||
export PYTHONPATH="$PIP_PREFIX/${python3.sitePackages}"
|
|
||||||
export PATH="$PIP_PREFIX/bin:$PATH"
|
|
||||||
# Avoids reproducibility issues with some Python packages
|
|
||||||
# See https://nixos.org/manual/nixpkgs/stable/#python-setup.py-bdist_wheel-cannot-create-.whl
|
|
||||||
unset SOURCE_DATE_EPOCH
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,6 @@ BuildRequires: pkgconfig(bluez)
|
|||||||
BuildRequires: pkgconfig(libusb-1.0)
|
BuildRequires: pkgconfig(libusb-1.0)
|
||||||
BuildRequires: libi2c-devel
|
BuildRequires: libi2c-devel
|
||||||
BuildRequires: pkgconfig(libuv)
|
BuildRequires: pkgconfig(libuv)
|
||||||
BuildRequires: pkgconfig(sqlite3)
|
|
||||||
# Web components:
|
# Web components:
|
||||||
BuildRequires: pkgconfig(openssl)
|
BuildRequires: pkgconfig(openssl)
|
||||||
BuildRequires: pkgconfig(liborcania)
|
BuildRequires: pkgconfig(liborcania)
|
||||||
@@ -96,9 +95,6 @@ cp -r bin/config.d/* %{buildroot}%{_sysconfdir}/meshtasticd/available.d
|
|||||||
# Install systemd service
|
# Install systemd service
|
||||||
install -D -m 0644 bin/meshtasticd.service %{buildroot}%{_unitdir}/meshtasticd.service
|
install -D -m 0644 bin/meshtasticd.service %{buildroot}%{_unitdir}/meshtasticd.service
|
||||||
|
|
||||||
# Install meshtasticd start wrapper
|
|
||||||
install -D -m 0755 bin/meshtasticd-start.sh %{buildroot}%{_bindir}/meshtasticd-start.sh
|
|
||||||
|
|
||||||
# Install the web files under /usr/share/meshtasticd/web
|
# Install the web files under /usr/share/meshtasticd/web
|
||||||
mkdir -p %{buildroot}%{_datadir}/meshtasticd/web
|
mkdir -p %{buildroot}%{_datadir}/meshtasticd/web
|
||||||
cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web
|
cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web
|
||||||
|
|||||||
@@ -43,11 +43,13 @@ class Esp32C3ExceptionDecoder(DeviceMonitorFilterBase):
|
|||||||
self.enabled = self.setup_paths()
|
self.enabled = self.setup_paths()
|
||||||
|
|
||||||
if self.config.get("env:" + self.environment, "build_type") != "debug":
|
if self.config.get("env:" + self.environment, "build_type") != "debug":
|
||||||
print("""
|
print(
|
||||||
|
"""
|
||||||
Please build project in debug configuration to get more details about an exception.
|
Please build project in debug configuration to get more details about an exception.
|
||||||
See https://docs.platformio.org/page/projectconf/build_configurations.html
|
See https://docs.platformio.org/page/projectconf/build_configurations.html
|
||||||
|
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
+19
-42
@@ -54,9 +54,7 @@ build_flags = -Wno-missing-field-initializers
|
|||||||
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
||||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||||
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
||||||
-DMESHTASTIC_EXCLUDE_POWERMON=1
|
|
||||||
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
||||||
-DLED_BUILTIN=-1
|
|
||||||
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
||||||
#-D OLED_PL=1
|
#-D OLED_PL=1
|
||||||
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
|
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
|
||||||
@@ -66,7 +64,7 @@ monitor_speed = 115200
|
|||||||
monitor_filters = direct
|
monitor_filters = direct
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
# 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/21e484f409cde18d44012caef84c244eb5ca28f3.zip
|
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/2887bf4a19f64d92c984dcc8fd5ca7429e425e4a.zip
|
||||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
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
|
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||||
@@ -105,22 +103,27 @@ lib_deps =
|
|||||||
thingsboard/TBPubSubClient@2.12.1
|
thingsboard/TBPubSubClient@2.12.1
|
||||||
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
|
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
|
||||||
arduino-libraries/NTPClient@3.2.1
|
arduino-libraries/NTPClient@3.2.1
|
||||||
|
|
||||||
; Extra TCP/IP networking libs for supported devices
|
|
||||||
[networking_extra]
|
|
||||||
lib_deps =
|
|
||||||
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
|
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
|
||||||
arcao/Syslog@2.0.0
|
arcao/Syslog@2.0.0
|
||||||
|
|
||||||
|
; Minimal networking libs for nrf52 (excludes Syslog to save flash)
|
||||||
|
[nrf52_networking_base]
|
||||||
|
lib_deps =
|
||||||
|
# renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient
|
||||||
|
thingsboard/TBPubSubClient@2.12.1
|
||||||
|
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
|
||||||
|
arduino-libraries/NTPClient@3.2.1
|
||||||
|
|
||||||
[radiolib_base]
|
[radiolib_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
|
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
|
||||||
jgromes/RadioLib@7.5.0
|
# jgromes/RadioLib@7.4.0
|
||||||
|
https://github.com/jgromes/RadioLib/archive/536c7267362e2c1345be7054ba45e503252975ff.zip
|
||||||
|
|
||||||
[device-ui_base]
|
[device-ui_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||||
https://github.com/meshtastic/device-ui/archive/6c75195e9987b7a49563232234f2f868dd343cae.zip
|
https://github.com/meshtastic/device-ui/archive/4fb5f24787caa841b58dbf623a52c4c5861d6722.zip
|
||||||
|
|
||||||
; Common libs for environmental measurements in telemetry module
|
; Common libs for environmental measurements in telemetry module
|
||||||
[environmental_base]
|
[environmental_base]
|
||||||
@@ -130,7 +133,7 @@ lib_deps =
|
|||||||
# renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor
|
# renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor
|
||||||
adafruit/Adafruit Unified Sensor@1.1.15
|
adafruit/Adafruit Unified Sensor@1.1.15
|
||||||
# renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library
|
# renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library
|
||||||
adafruit/Adafruit BMP280 Library@3.0.0
|
adafruit/Adafruit BMP280 Library@2.6.8
|
||||||
# renovate: datasource=custom.pio depName=Adafruit BMP085 packageName=adafruit/library/Adafruit BMP085 Library
|
# renovate: datasource=custom.pio depName=Adafruit BMP085 packageName=adafruit/library/Adafruit BMP085 Library
|
||||||
adafruit/Adafruit BMP085 Library@1.2.4
|
adafruit/Adafruit BMP085 Library@1.2.4
|
||||||
# renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library
|
# renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library
|
||||||
@@ -143,8 +146,10 @@ lib_deps =
|
|||||||
adafruit/Adafruit INA260 Library@1.5.3
|
adafruit/Adafruit INA260 Library@1.5.3
|
||||||
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
||||||
adafruit/Adafruit INA219@1.2.3
|
adafruit/Adafruit INA219@1.2.3
|
||||||
|
# renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor
|
||||||
|
adafruit/Adafruit PM25 AQI Sensor@2.0.0
|
||||||
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
|
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
|
||||||
adafruit/Adafruit MPU6050@2.2.8
|
adafruit/Adafruit MPU6050@2.2.6
|
||||||
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
|
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
|
||||||
adafruit/Adafruit LIS3DH@1.3.0
|
adafruit/Adafruit LIS3DH@1.3.0
|
||||||
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
|
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
|
||||||
@@ -157,8 +162,8 @@ lib_deps =
|
|||||||
emotibit/EmotiBit MLX90632@1.0.8
|
emotibit/EmotiBit MLX90632@1.0.8
|
||||||
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
|
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
|
||||||
adafruit/Adafruit MLX90614 Library@2.1.5
|
adafruit/Adafruit MLX90614 Library@2.1.5
|
||||||
# renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow
|
# renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221
|
||||||
https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip
|
https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a
|
||||||
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
|
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
|
||||||
mprograms/QMC5883LCompass@1.2.3
|
mprograms/QMC5883LCompass@1.2.3
|
||||||
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU
|
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU
|
||||||
@@ -166,7 +171,7 @@ lib_deps =
|
|||||||
# renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master
|
# renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master
|
||||||
https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip
|
https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip
|
||||||
# renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226
|
# renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226
|
||||||
robtillaart/INA226@0.6.6
|
robtillaart/INA226@0.6.5
|
||||||
# renovate: datasource=custom.pio depName=SparkFun MAX3010x packageName=sparkfun/library/SparkFun MAX3010x Pulse and Proximity Sensor Library
|
# renovate: datasource=custom.pio depName=SparkFun MAX3010x packageName=sparkfun/library/SparkFun MAX3010x Pulse and Proximity Sensor Library
|
||||||
sparkfun/SparkFun MAX3010x Pulse and Proximity Sensor Library@1.1.2
|
sparkfun/SparkFun MAX3010x Pulse and Proximity Sensor Library@1.1.2
|
||||||
# renovate: datasource=custom.pio depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/library/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library
|
# renovate: datasource=custom.pio depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/library/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library
|
||||||
@@ -213,31 +218,3 @@ lib_deps =
|
|||||||
sensirion/Sensirion Core@0.7.2
|
sensirion/Sensirion Core@0.7.2
|
||||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||||
|
|
||||||
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
|
||||||
[environmental_extra_no_bsec]
|
|
||||||
lib_deps =
|
|
||||||
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
|
|
||||||
adafruit/Adafruit BMP3XX Library@2.1.6
|
|
||||||
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
|
|
||||||
adafruit/Adafruit MAX1704X@1.0.3
|
|
||||||
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
|
|
||||||
adafruit/Adafruit SHTC3 Library@1.0.2
|
|
||||||
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
|
|
||||||
adafruit/Adafruit LPS2X@2.0.6
|
|
||||||
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
|
|
||||||
adafruit/Adafruit SHT31 Library@2.2.2
|
|
||||||
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
|
|
||||||
adafruit/Adafruit VEML7700 Library@2.1.6
|
|
||||||
# renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library
|
|
||||||
adafruit/Adafruit SHT4x Library@1.0.5
|
|
||||||
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
|
|
||||||
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
|
|
||||||
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
|
|
||||||
closedcube/ClosedCube OPT3001@1.1.2
|
|
||||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
|
||||||
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
|
||||||
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
|
||||||
sensirion/Sensirion Core@0.7.2
|
|
||||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
|
||||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
|
||||||
|
|||||||
+1
-1
Submodule protobufs updated: e80cb2e410...4095e59890
@@ -1,12 +0,0 @@
|
|||||||
(import (
|
|
||||||
let
|
|
||||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
|
||||||
nodeName = lock.nodes.root.inputs.flake-compat;
|
|
||||||
in
|
|
||||||
fetchTarball {
|
|
||||||
url =
|
|
||||||
lock.nodes.${nodeName}.locked.url
|
|
||||||
or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
|
|
||||||
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
|
||||||
}
|
|
||||||
) { src = ./.; }).shellNix
|
|
||||||
@@ -41,8 +41,7 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if HAS_NETWORKING
|
#if HAS_NETWORKING
|
||||||
namespace meshtastic
|
|
||||||
{
|
|
||||||
Syslog::Syslog(UDP &client)
|
Syslog::Syslog(UDP &client)
|
||||||
{
|
{
|
||||||
this->_client = &client;
|
this->_client = &client;
|
||||||
@@ -196,6 +195,4 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}; // namespace meshtastic
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -162,8 +162,6 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
|
|||||||
|
|
||||||
#if HAS_NETWORKING
|
#if HAS_NETWORKING
|
||||||
|
|
||||||
namespace meshtastic
|
|
||||||
{
|
|
||||||
class Syslog
|
class Syslog
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
@@ -197,6 +195,4 @@ class Syslog
|
|||||||
bool vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) __attribute__((format(printf, 3, 0)));
|
bool vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) __attribute__((format(printf, 3, 0)));
|
||||||
};
|
};
|
||||||
|
|
||||||
}; // namespace meshtastic
|
#endif // HAS_NETWORKING
|
||||||
|
|
||||||
#endif // HAS_NETWORKING
|
|
||||||
@@ -31,9 +31,6 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
|||||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
|
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
|
||||||
return useShortName ? "LongF" : "LongFast";
|
return useShortName ? "LongF" : "LongFast";
|
||||||
break;
|
break;
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
|
|
||||||
return useShortName ? "LongT" : "LongTurbo";
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
|
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
|
||||||
return useShortName ? "LongM" : "LongMod";
|
return useShortName ? "LongM" : "LongMod";
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
*/
|
*/
|
||||||
#include "FSCommon.h"
|
#include "FSCommon.h"
|
||||||
#include "SPILock.h"
|
#include "SPILock.h"
|
||||||
|
#include "SafeFile.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include <pb_decode.h>
|
||||||
|
#include <pb_encode.h>
|
||||||
|
|
||||||
// Software SPI is used by MUI so disable SD card here until it's also implemented
|
// Software SPI is used by MUI so disable SD card here until it's also implemented
|
||||||
#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI)
|
#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI)
|
||||||
@@ -335,4 +338,63 @@ void setupSDCard()
|
|||||||
LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024)));
|
LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024)));
|
||||||
LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024)));
|
LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024)));
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load a protobuf from a file, return LoadFileResult */
|
||||||
|
LoadFileResult loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, void *dest_struct)
|
||||||
|
{
|
||||||
|
LoadFileResult state = LoadFileResult::OTHER_FAILURE;
|
||||||
|
#ifdef FSCom
|
||||||
|
concurrency::LockGuard g(spiLock);
|
||||||
|
|
||||||
|
auto f = FSCom.open(filename, FILE_O_READ);
|
||||||
|
|
||||||
|
if (f) {
|
||||||
|
LOG_INFO("Load %s", filename);
|
||||||
|
pb_istream_t stream = {&readcb, &f, protoSize};
|
||||||
|
if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object
|
||||||
|
memset(dest_struct, 0, objSize);
|
||||||
|
if (!pb_decode(&stream, fields, dest_struct)) {
|
||||||
|
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
|
||||||
|
state = LoadFileResult::DECODE_FAILED;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("Loaded %s successfully", filename);
|
||||||
|
state = LoadFileResult::LOAD_SUCCESS;
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("Could not open / read %s", filename);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
LOG_ERROR("ERROR: Filesystem not implemented");
|
||||||
|
state = LoadFileResult::NO_FILESYSTEM;
|
||||||
|
#endif
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save a protobuf from a file, return true for success */
|
||||||
|
bool saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, bool fullAtomic)
|
||||||
|
{
|
||||||
|
bool okay = false;
|
||||||
|
#ifdef FSCom
|
||||||
|
auto f = SafeFile(filename, fullAtomic);
|
||||||
|
|
||||||
|
LOG_INFO("Save %s", filename);
|
||||||
|
pb_ostream_t stream = {&writecb, static_cast<Print *>(&f), protoSize};
|
||||||
|
|
||||||
|
if (!pb_encode(&stream, fields, dest_struct)) {
|
||||||
|
LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream));
|
||||||
|
} else {
|
||||||
|
okay = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writeSucceeded = f.close();
|
||||||
|
|
||||||
|
if (!okay || !writeSucceeded) {
|
||||||
|
LOG_ERROR("Can't write prefs!");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
LOG_ERROR("ERROR: Filesystem not implemented");
|
||||||
|
#endif
|
||||||
|
return okay;
|
||||||
}
|
}
|
||||||
+18
-1
@@ -3,6 +3,19 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
enum LoadFileResult {
|
||||||
|
// Successfully opened the file
|
||||||
|
LOAD_SUCCESS = 1,
|
||||||
|
// File does not exist
|
||||||
|
NOT_FOUND = 2,
|
||||||
|
// Device does not have a filesystem
|
||||||
|
NO_FILESYSTEM = 3,
|
||||||
|
// File exists, but could not decode protobufs
|
||||||
|
DECODE_FAILED = 4,
|
||||||
|
// File exists, but open failed for some reason
|
||||||
|
OTHER_FAILURE = 5
|
||||||
|
};
|
||||||
|
|
||||||
// Cross platform filesystem API
|
// Cross platform filesystem API
|
||||||
|
|
||||||
#if defined(ARCH_PORTDUINO)
|
#if defined(ARCH_PORTDUINO)
|
||||||
@@ -55,4 +68,8 @@ bool renameFile(const char *pathFrom, const char *pathTo);
|
|||||||
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels);
|
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels);
|
||||||
void listDir(const char *dirname, uint8_t levels, bool del = false);
|
void listDir(const char *dirname, uint8_t levels, bool del = false);
|
||||||
void rmDir(const char *dirname);
|
void rmDir(const char *dirname);
|
||||||
void setupSDCard();
|
void setupSDCard();
|
||||||
|
LoadFileResult loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, void *dest_struct);
|
||||||
|
|
||||||
|
bool saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct,
|
||||||
|
bool fullAtomic = true);
|
||||||
@@ -1,514 +0,0 @@
|
|||||||
#include "configuration.h"
|
|
||||||
#if HAS_SCREEN
|
|
||||||
#include "FSCommon.h"
|
|
||||||
#include "MessageStore.h"
|
|
||||||
#include "NodeDB.h"
|
|
||||||
#include "SPILock.h"
|
|
||||||
#include "SafeFile.h"
|
|
||||||
#include "gps/RTC.h"
|
|
||||||
#include "graphics/draw/MessageRenderer.h"
|
|
||||||
#include <cstring> // memcpy
|
|
||||||
|
|
||||||
#ifndef MESSAGE_TEXT_POOL_SIZE
|
|
||||||
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Default autosave interval 2 hours, override per device later with -DMESSAGE_AUTOSAVE_INTERVAL_SEC=300 (etc)
|
|
||||||
#ifndef MESSAGE_AUTOSAVE_INTERVAL_SEC
|
|
||||||
#define MESSAGE_AUTOSAVE_INTERVAL_SEC (2 * 60 * 60)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Global message text pool and state
|
|
||||||
static char *g_messagePool = nullptr;
|
|
||||||
static size_t g_poolWritePos = 0;
|
|
||||||
|
|
||||||
// Reset pool (called on boot or clear)
|
|
||||||
static inline void resetMessagePool()
|
|
||||||
{
|
|
||||||
if (!g_messagePool) {
|
|
||||||
g_messagePool = static_cast<char *>(malloc(MESSAGE_TEXT_POOL_SIZE));
|
|
||||||
if (!g_messagePool) {
|
|
||||||
LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g_poolWritePos = 0;
|
|
||||||
memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate text in pool and return offset
|
|
||||||
// If not enough space remains, wrap around (ring buffer style)
|
|
||||||
static inline uint16_t storeTextInPool(const char *src, size_t len)
|
|
||||||
{
|
|
||||||
if (len >= MAX_MESSAGE_SIZE)
|
|
||||||
len = MAX_MESSAGE_SIZE - 1;
|
|
||||||
|
|
||||||
// Wrap pool if out of space
|
|
||||||
if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) {
|
|
||||||
g_poolWritePos = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t offset = g_poolWritePos;
|
|
||||||
memcpy(&g_messagePool[g_poolWritePos], src, len);
|
|
||||||
g_messagePool[g_poolWritePos + len] = '\0';
|
|
||||||
g_poolWritePos += (len + 1);
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve a const pointer to message text by offset
|
|
||||||
static inline const char *getTextFromPool(uint16_t offset)
|
|
||||||
{
|
|
||||||
if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE)
|
|
||||||
return "";
|
|
||||||
return &g_messagePool[offset];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: assign a timestamp (RTC if available, else boot-relative)
|
|
||||||
static inline void assignTimestamp(StoredMessage &sm)
|
|
||||||
{
|
|
||||||
uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
|
|
||||||
if (nowSecs) {
|
|
||||||
sm.timestamp = nowSecs;
|
|
||||||
sm.isBootRelative = false;
|
|
||||||
} else {
|
|
||||||
sm.timestamp = millis() / 1000;
|
|
||||||
sm.isBootRelative = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic push with cap (used by live + persisted queues)
|
|
||||||
template <typename T> static inline void pushWithLimit(std::deque<T> &queue, const T &msg)
|
|
||||||
{
|
|
||||||
if (queue.size() >= MAX_MESSAGES_SAVED)
|
|
||||||
queue.pop_front();
|
|
||||||
queue.push_back(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T> static inline void pushWithLimit(std::deque<T> &queue, T &&msg)
|
|
||||||
{
|
|
||||||
if (queue.size() >= MAX_MESSAGES_SAVED)
|
|
||||||
queue.pop_front();
|
|
||||||
queue.emplace_back(std::move(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageStore::MessageStore(const std::string &label)
|
|
||||||
{
|
|
||||||
filename = "/Messages_" + label + ".msgs";
|
|
||||||
resetMessagePool(); // initialize text pool on boot
|
|
||||||
}
|
|
||||||
|
|
||||||
// Live message handling (RAM only)
|
|
||||||
void MessageStore::addLiveMessage(StoredMessage &&msg)
|
|
||||||
{
|
|
||||||
pushWithLimit(liveMessages, std::move(msg));
|
|
||||||
}
|
|
||||||
void MessageStore::addLiveMessage(const StoredMessage &msg)
|
|
||||||
{
|
|
||||||
pushWithLimit(liveMessages, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if ENABLE_MESSAGE_PERSISTENCE
|
|
||||||
static bool g_messageStoreHasUnsavedChanges = false;
|
|
||||||
static uint32_t g_lastAutoSaveMs = 0; // last time we actually saved
|
|
||||||
|
|
||||||
static inline uint32_t autosaveIntervalMs()
|
|
||||||
{
|
|
||||||
uint32_t sec = (uint32_t)MESSAGE_AUTOSAVE_INTERVAL_SEC;
|
|
||||||
if (sec < 60)
|
|
||||||
sec = 60;
|
|
||||||
return sec * 1000UL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline bool reachedMs(uint32_t now, uint32_t target)
|
|
||||||
{
|
|
||||||
return (int32_t)(now - target) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark new messages in RAM that need to be saved later
|
|
||||||
static inline void markMessageStoreUnsaved()
|
|
||||||
{
|
|
||||||
g_messageStoreHasUnsavedChanges = true;
|
|
||||||
|
|
||||||
if (g_lastAutoSaveMs == 0) {
|
|
||||||
g_lastAutoSaveMs = millis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called periodically from the main loop in main.cpp
|
|
||||||
static inline void autosaveTick(MessageStore *store)
|
|
||||||
{
|
|
||||||
if (!store)
|
|
||||||
return;
|
|
||||||
|
|
||||||
uint32_t now = millis();
|
|
||||||
|
|
||||||
if (g_lastAutoSaveMs == 0) {
|
|
||||||
g_lastAutoSaveMs = now;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reachedMs(now, g_lastAutoSaveMs + autosaveIntervalMs()))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Autosave interval reached, only save if there are unsaved messages.
|
|
||||||
if (g_messageStoreHasUnsavedChanges) {
|
|
||||||
LOG_INFO("Autosaving MessageStore to flash");
|
|
||||||
store->saveToFlash();
|
|
||||||
} else {
|
|
||||||
LOG_INFO("Autosave skipped, no changes to save");
|
|
||||||
g_lastAutoSaveMs = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Add from incoming/outgoing packet
|
|
||||||
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
|
|
||||||
{
|
|
||||||
StoredMessage sm;
|
|
||||||
assignTimestamp(sm);
|
|
||||||
sm.channelIndex = packet.channel;
|
|
||||||
|
|
||||||
const char *payload = reinterpret_cast<const char *>(packet.decoded.payload.bytes);
|
|
||||||
size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1);
|
|
||||||
sm.textOffset = storeTextInPool(payload, len);
|
|
||||||
sm.textLength = len;
|
|
||||||
|
|
||||||
// Determine sender
|
|
||||||
uint32_t localNode = nodeDB->getNodeNum();
|
|
||||||
sm.sender = (packet.from == 0) ? localNode : packet.from;
|
|
||||||
|
|
||||||
sm.dest = packet.to;
|
|
||||||
|
|
||||||
bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST);
|
|
||||||
|
|
||||||
if (packet.from == 0) {
|
|
||||||
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
|
|
||||||
sm.ackStatus = AckStatus::NONE;
|
|
||||||
} else {
|
|
||||||
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
|
|
||||||
sm.ackStatus = AckStatus::ACKED;
|
|
||||||
}
|
|
||||||
|
|
||||||
addLiveMessage(sm);
|
|
||||||
|
|
||||||
#if ENABLE_MESSAGE_PERSISTENCE
|
|
||||||
markMessageStoreUnsaved();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return liveMessages.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Outgoing/manual message
|
|
||||||
void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text)
|
|
||||||
{
|
|
||||||
StoredMessage sm;
|
|
||||||
|
|
||||||
// Always use our local time (helper handles RTC vs boot time)
|
|
||||||
assignTimestamp(sm);
|
|
||||||
|
|
||||||
sm.sender = sender;
|
|
||||||
sm.channelIndex = channelIndex;
|
|
||||||
sm.textOffset = storeTextInPool(text.c_str(), text.size());
|
|
||||||
sm.textLength = text.size();
|
|
||||||
|
|
||||||
// Use the provided destination
|
|
||||||
sm.dest = sender;
|
|
||||||
sm.type = MessageType::DM_TO_US;
|
|
||||||
|
|
||||||
// Outgoing messages always start with unknown ack status
|
|
||||||
sm.ackStatus = AckStatus::NONE;
|
|
||||||
|
|
||||||
addLiveMessage(sm);
|
|
||||||
|
|
||||||
#if ENABLE_MESSAGE_PERSISTENCE
|
|
||||||
markMessageStoreUnsaved();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
#if ENABLE_MESSAGE_PERSISTENCE
|
|
||||||
|
|
||||||
// Compact, fixed-size on-flash representation using offset + length
|
|
||||||
struct __attribute__((packed)) StoredMessageRecord {
|
|
||||||
uint32_t timestamp;
|
|
||||||
uint32_t sender;
|
|
||||||
uint8_t channelIndex;
|
|
||||||
uint32_t dest;
|
|
||||||
uint8_t isBootRelative;
|
|
||||||
uint8_t ackStatus; // static_cast<uint8_t>(AckStatus)
|
|
||||||
uint8_t type; // static_cast<uint8_t>(MessageType)
|
|
||||||
uint16_t textLength; // message length
|
|
||||||
char text[MAX_MESSAGE_SIZE]; // store actual text here
|
|
||||||
};
|
|
||||||
|
|
||||||
// Serialize one StoredMessage to flash
|
|
||||||
static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m)
|
|
||||||
{
|
|
||||||
StoredMessageRecord rec = {};
|
|
||||||
rec.timestamp = m.timestamp;
|
|
||||||
rec.sender = m.sender;
|
|
||||||
rec.channelIndex = m.channelIndex;
|
|
||||||
rec.dest = m.dest;
|
|
||||||
rec.isBootRelative = m.isBootRelative;
|
|
||||||
rec.ackStatus = static_cast<uint8_t>(m.ackStatus);
|
|
||||||
rec.type = static_cast<uint8_t>(m.type);
|
|
||||||
rec.textLength = m.textLength;
|
|
||||||
|
|
||||||
// Copy the actual text into the record from RAM pool
|
|
||||||
const char *txt = getTextFromPool(m.textOffset);
|
|
||||||
strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1);
|
|
||||||
rec.text[MAX_MESSAGE_SIZE - 1] = '\0';
|
|
||||||
|
|
||||||
f.write(reinterpret_cast<const uint8_t *>(&rec), sizeof(rec));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deserialize one StoredMessage from flash; returns false on short read
|
|
||||||
static inline bool readMessageRecord(File &f, StoredMessage &m)
|
|
||||||
{
|
|
||||||
StoredMessageRecord rec = {};
|
|
||||||
if (f.readBytes(reinterpret_cast<char *>(&rec), sizeof(rec)) != sizeof(rec))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
m.timestamp = rec.timestamp;
|
|
||||||
m.sender = rec.sender;
|
|
||||||
m.channelIndex = rec.channelIndex;
|
|
||||||
m.dest = rec.dest;
|
|
||||||
m.isBootRelative = rec.isBootRelative;
|
|
||||||
m.ackStatus = static_cast<AckStatus>(rec.ackStatus);
|
|
||||||
m.type = static_cast<MessageType>(rec.type);
|
|
||||||
m.textLength = rec.textLength;
|
|
||||||
|
|
||||||
// 💡 Re-store text into pool and update offset
|
|
||||||
m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1);
|
|
||||||
m.textOffset = storeTextInPool(rec.text, m.textLength);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageStore::saveToFlash()
|
|
||||||
{
|
|
||||||
#ifdef FSCom
|
|
||||||
// Ensure root exists
|
|
||||||
spiLock->lock();
|
|
||||||
FSCom.mkdir("/");
|
|
||||||
spiLock->unlock();
|
|
||||||
|
|
||||||
SafeFile f(filename.c_str(), false);
|
|
||||||
|
|
||||||
spiLock->lock();
|
|
||||||
uint8_t count = static_cast<uint8_t>(liveMessages.size());
|
|
||||||
if (count > MAX_MESSAGES_SAVED)
|
|
||||||
count = MAX_MESSAGES_SAVED;
|
|
||||||
f.write(&count, 1);
|
|
||||||
|
|
||||||
for (uint8_t i = 0; i < count; ++i) {
|
|
||||||
writeMessageRecord(f, liveMessages[i]);
|
|
||||||
}
|
|
||||||
spiLock->unlock();
|
|
||||||
|
|
||||||
f.close();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Reset autosave state after any save
|
|
||||||
g_messageStoreHasUnsavedChanges = false;
|
|
||||||
g_lastAutoSaveMs = millis();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageStore::loadFromFlash()
|
|
||||||
{
|
|
||||||
std::deque<StoredMessage>().swap(liveMessages);
|
|
||||||
resetMessagePool(); // reset pool when loading
|
|
||||||
|
|
||||||
#ifdef FSCom
|
|
||||||
concurrency::LockGuard guard(spiLock);
|
|
||||||
|
|
||||||
if (!FSCom.exists(filename.c_str()))
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
|
||||||
if (!f)
|
|
||||||
return;
|
|
||||||
|
|
||||||
uint8_t count = 0;
|
|
||||||
f.readBytes(reinterpret_cast<char *>(&count), 1);
|
|
||||||
if (count > MAX_MESSAGES_SAVED)
|
|
||||||
count = MAX_MESSAGES_SAVED;
|
|
||||||
|
|
||||||
for (uint8_t i = 0; i < count; ++i) {
|
|
||||||
StoredMessage m;
|
|
||||||
if (!readMessageRecord(f, m))
|
|
||||||
break;
|
|
||||||
liveMessages.push_back(m);
|
|
||||||
}
|
|
||||||
|
|
||||||
f.close();
|
|
||||||
#endif
|
|
||||||
// Loading messages does not trigger an autosave
|
|
||||||
g_messageStoreHasUnsavedChanges = false;
|
|
||||||
g_lastAutoSaveMs = millis();
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
|
||||||
// If persistence is disabled, these functions become no-ops
|
|
||||||
void MessageStore::saveToFlash() {}
|
|
||||||
void MessageStore::loadFromFlash() {}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Clear all messages (RAM + persisted queue)
|
|
||||||
void MessageStore::clearAllMessages()
|
|
||||||
{
|
|
||||||
std::deque<StoredMessage>().swap(liveMessages);
|
|
||||||
resetMessagePool();
|
|
||||||
|
|
||||||
#ifdef FSCom
|
|
||||||
SafeFile f(filename.c_str(), false);
|
|
||||||
uint8_t count = 0;
|
|
||||||
f.write(&count, 1); // write "0 messages"
|
|
||||||
f.close();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if ENABLE_MESSAGE_PERSISTENCE
|
|
||||||
g_messageStoreHasUnsavedChanges = false;
|
|
||||||
g_lastAutoSaveMs = millis();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal helper: erase first or last message matching a predicate
|
|
||||||
template <typename Predicate> static void eraseIf(std::deque<StoredMessage> &deque, Predicate pred, bool fromBack = false)
|
|
||||||
{
|
|
||||||
if (fromBack) {
|
|
||||||
// Iterate from the back and erase all matches from the end
|
|
||||||
for (auto it = deque.rbegin(); it != deque.rend();) {
|
|
||||||
if (pred(*it)) {
|
|
||||||
it = std::deque<StoredMessage>::reverse_iterator(deque.erase(std::next(it).base()));
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Manual forward search to erase all matches
|
|
||||||
for (auto it = deque.begin(); it != deque.end();) {
|
|
||||||
if (pred(*it)) {
|
|
||||||
it = deque.erase(it);
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete oldest message (RAM + persisted queue)
|
|
||||||
void MessageStore::deleteOldestMessage()
|
|
||||||
{
|
|
||||||
eraseIf(liveMessages, [](StoredMessage &) { return true; });
|
|
||||||
saveToFlash();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete oldest message in a specific channel
|
|
||||||
void MessageStore::deleteOldestMessageInChannel(uint8_t channel)
|
|
||||||
{
|
|
||||||
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
|
|
||||||
eraseIf(liveMessages, pred);
|
|
||||||
saveToFlash();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageStore::deleteAllMessagesInChannel(uint8_t channel)
|
|
||||||
{
|
|
||||||
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
|
|
||||||
eraseIf(liveMessages, pred, false /* delete ALL, not just first */);
|
|
||||||
saveToFlash();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageStore::deleteAllMessagesWithPeer(uint32_t peer)
|
|
||||||
{
|
|
||||||
uint32_t local = nodeDB->getNodeNum();
|
|
||||||
auto pred = [&](const StoredMessage &m) {
|
|
||||||
if (m.type != MessageType::DM_TO_US)
|
|
||||||
return false;
|
|
||||||
uint32_t other = (m.sender == local) ? m.dest : m.sender;
|
|
||||||
return other == peer;
|
|
||||||
};
|
|
||||||
eraseIf(liveMessages, pred, false);
|
|
||||||
saveToFlash();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete oldest message in a direct chat with a node
|
|
||||||
void MessageStore::deleteOldestMessageWithPeer(uint32_t peer)
|
|
||||||
{
|
|
||||||
auto pred = [peer](const StoredMessage &m) {
|
|
||||||
if (m.type != MessageType::DM_TO_US)
|
|
||||||
return false;
|
|
||||||
uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
|
|
||||||
return other == peer;
|
|
||||||
};
|
|
||||||
eraseIf(liveMessages, pred);
|
|
||||||
saveToFlash();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::deque<StoredMessage> MessageStore::getChannelMessages(uint8_t channel) const
|
|
||||||
{
|
|
||||||
std::deque<StoredMessage> result;
|
|
||||||
for (const auto &m : liveMessages) {
|
|
||||||
if (m.type == MessageType::BROADCAST && m.channelIndex == channel) {
|
|
||||||
result.push_back(m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::deque<StoredMessage> MessageStore::getDirectMessages() const
|
|
||||||
{
|
|
||||||
std::deque<StoredMessage> result;
|
|
||||||
for (const auto &m : liveMessages) {
|
|
||||||
if (m.type == MessageType::DM_TO_US) {
|
|
||||||
result.push_back(m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upgrade boot-relative timestamps once RTC is valid
|
|
||||||
// Only same-boot boot-relative messages are healed.
|
|
||||||
// Persisted boot-relative messages from old boots stay ??? forever.
|
|
||||||
void MessageStore::upgradeBootRelativeTimestamps()
|
|
||||||
{
|
|
||||||
uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
|
|
||||||
if (nowSecs == 0)
|
|
||||||
return; // Still no valid RTC
|
|
||||||
|
|
||||||
uint32_t bootNow = millis() / 1000;
|
|
||||||
|
|
||||||
auto fix = [&](std::deque<StoredMessage> &dq) {
|
|
||||||
for (auto &m : dq) {
|
|
||||||
if (m.isBootRelative && m.timestamp <= bootNow) {
|
|
||||||
uint32_t bootOffset = nowSecs - bootNow;
|
|
||||||
m.timestamp += bootOffset;
|
|
||||||
m.isBootRelative = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fix(liveMessages);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *MessageStore::getText(const StoredMessage &msg)
|
|
||||||
{
|
|
||||||
// Wrapper around the internal helper
|
|
||||||
return getTextFromPool(msg.textOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t MessageStore::storeText(const char *src, size_t len)
|
|
||||||
{
|
|
||||||
// Wrapper around the internal helper
|
|
||||||
return storeTextInPool(src, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if ENABLE_MESSAGE_PERSISTENCE
|
|
||||||
void messageStoreAutosaveTick()
|
|
||||||
{
|
|
||||||
// Called from the main loop to check autosave timing
|
|
||||||
autosaveTick(&messageStore);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Global definition
|
|
||||||
MessageStore messageStore("default");
|
|
||||||
#endif
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#if HAS_SCREEN
|
|
||||||
|
|
||||||
// Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints
|
|
||||||
#if defined(HELTEC_MESH_SOLAR)
|
|
||||||
#define LOG_DEBUG(...)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Enable or disable message persistence (flash storage)
|
|
||||||
// Define -DENABLE_MESSAGE_PERSISTENCE=0 in build_flags to disable it entirely
|
|
||||||
#ifndef ENABLE_MESSAGE_PERSISTENCE
|
|
||||||
#define ENABLE_MESSAGE_PERSISTENCE 1
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
|
||||||
#include <cstdint>
|
|
||||||
#include <deque>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
// How many messages are stored (RAM + flash).
|
|
||||||
// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage.
|
|
||||||
#ifndef MESSAGE_HISTORY_LIMIT
|
|
||||||
#define MESSAGE_HISTORY_LIMIT 20
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Internal alias used everywhere in code – do NOT redefine elsewhere.
|
|
||||||
#define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT
|
|
||||||
|
|
||||||
// Maximum text payload size per message in bytes.
|
|
||||||
// This still defines the max message length, but we no longer reserve this space per message.
|
|
||||||
#define MAX_MESSAGE_SIZE 220
|
|
||||||
|
|
||||||
// Total shared text pool size for all messages combined.
|
|
||||||
// The text pool is RAM-only. Text is re-stored from flash into the pool on boot.
|
|
||||||
#ifndef MESSAGE_TEXT_POOL_SIZE
|
|
||||||
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Explicit message classification
|
|
||||||
enum class MessageType : uint8_t {
|
|
||||||
BROADCAST = 0, // broadcast message
|
|
||||||
DM_TO_US = 1 // direct message addressed to this node
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delivery status for messages we sent
|
|
||||||
enum class AckStatus : uint8_t {
|
|
||||||
NONE = 0, // just sent, waiting (no symbol shown)
|
|
||||||
ACKED = 1, // got a valid ACK from destination
|
|
||||||
NACKED = 2, // explicitly failed
|
|
||||||
TIMEOUT = 3, // no ACK after retry window
|
|
||||||
RELAYED = 4 // got an ACK from relay, not destination
|
|
||||||
};
|
|
||||||
|
|
||||||
struct StoredMessage {
|
|
||||||
uint32_t timestamp; // When message was created (secs since boot or RTC)
|
|
||||||
uint32_t sender; // NodeNum of sender
|
|
||||||
uint8_t channelIndex; // Channel index used
|
|
||||||
uint32_t dest; // Destination node (broadcast or direct)
|
|
||||||
MessageType type; // Derived from dest (explicit classification)
|
|
||||||
bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute
|
|
||||||
AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages)
|
|
||||||
|
|
||||||
// Text storage metadata — rebuilt from flash at boot
|
|
||||||
uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash())
|
|
||||||
uint16_t textLength; // Length of text in bytes
|
|
||||||
|
|
||||||
// Default constructor initializes all fields safely
|
|
||||||
StoredMessage()
|
|
||||||
: timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false),
|
|
||||||
ackStatus(AckStatus::NONE), textOffset(0), textLength(0)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class MessageStore
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit MessageStore(const std::string &label);
|
|
||||||
|
|
||||||
// Live RAM methods (always current, used by UI and runtime)
|
|
||||||
void addLiveMessage(StoredMessage &&msg);
|
|
||||||
void addLiveMessage(const StoredMessage &msg); // convenience overload
|
|
||||||
const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; }
|
|
||||||
|
|
||||||
// Add new messages from packets or manual input
|
|
||||||
const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only
|
|
||||||
void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add
|
|
||||||
|
|
||||||
// Persistence methods (used only on boot/shutdown)
|
|
||||||
void saveToFlash(); // Save messages to flash
|
|
||||||
void loadFromFlash(); // Load messages from flash
|
|
||||||
|
|
||||||
// Clear all messages (RAM + persisted queue + text pool)
|
|
||||||
void clearAllMessages();
|
|
||||||
|
|
||||||
// Delete helpers
|
|
||||||
void deleteOldestMessage(); // remove oldest from RAM (and flash on save)
|
|
||||||
void deleteOldestMessageInChannel(uint8_t channel);
|
|
||||||
void deleteOldestMessageWithPeer(uint32_t peer);
|
|
||||||
void deleteAllMessagesInChannel(uint8_t channel);
|
|
||||||
void deleteAllMessagesWithPeer(uint32_t peer);
|
|
||||||
|
|
||||||
// Unified accessor (for UI code, defaults to RAM buffer)
|
|
||||||
const std::deque<StoredMessage> &getMessages() const { return liveMessages; }
|
|
||||||
|
|
||||||
// Helper filters for future use
|
|
||||||
std::deque<StoredMessage> getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel
|
|
||||||
std::deque<StoredMessage> getDirectMessages() const; // Only direct messages
|
|
||||||
|
|
||||||
// Upgrade boot-relative timestamps once RTC is valid
|
|
||||||
void upgradeBootRelativeTimestamps();
|
|
||||||
|
|
||||||
// Retrieve the C-string text for a stored message
|
|
||||||
static const char *getText(const StoredMessage &msg);
|
|
||||||
|
|
||||||
// Allocate text into pool (used by sender-side code)
|
|
||||||
static uint16_t storeText(const char *src, size_t len);
|
|
||||||
|
|
||||||
// Used when loading from flash to rebuild the text pool
|
|
||||||
static uint16_t rebuildTextFromFlash(const char *src, size_t len);
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::deque<StoredMessage> liveMessages; // Single in-RAM message buffer (also used for persistence)
|
|
||||||
std::string filename; // Flash filename for persistence
|
|
||||||
};
|
|
||||||
|
|
||||||
#if ENABLE_MESSAGE_PERSISTENCE
|
|
||||||
// Called periodically from main loop to trigger time based autosave
|
|
||||||
void messageStoreAutosaveTick();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Global instance (defined in MessageStore.cpp)
|
|
||||||
extern MessageStore messageStore;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
+93
-263
@@ -1,20 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* @file Power.cpp
|
* @file Power.cpp
|
||||||
* @brief This file contains the implementation of the Power class, which is
|
* @brief This file contains the implementation of the Power class, which is responsible for managing power-related functionality
|
||||||
* responsible for managing power-related functionality of the device. It
|
* of the device. It includes battery level sensing, power management unit (PMU) control, and power state machine management. The
|
||||||
* includes battery level sensing, power management unit (PMU) control, and
|
* Power class is used by the main device class to manage power-related functionality.
|
||||||
* power state machine management. The Power class is used by the main device
|
|
||||||
* class to manage power-related functionality.
|
|
||||||
*
|
*
|
||||||
* The file also includes implementations of various battery level sensors, such
|
* The file also includes implementations of various battery level sensors, such as the AnalogBatteryLevel class, which assumes
|
||||||
* as the AnalogBatteryLevel class, which assumes the battery voltage is
|
* the battery voltage is attached via a voltage-divider to an analog input.
|
||||||
* attached via a voltage-divider to an analog input.
|
|
||||||
*
|
*
|
||||||
* This file is part of the Meshtastic project.
|
* This file is part of the Meshtastic project.
|
||||||
* For more information, see: https://meshtastic.org/
|
* For more information, see: https://meshtastic.org/
|
||||||
*/
|
*/
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
#include "MessageStore.h"
|
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "Throttle.h"
|
#include "Throttle.h"
|
||||||
@@ -22,7 +18,6 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include "power/PowerHAL.h"
|
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
|
|
||||||
#if defined(ARCH_PORTDUINO)
|
#if defined(ARCH_PORTDUINO)
|
||||||
@@ -175,12 +170,22 @@ Power *power;
|
|||||||
|
|
||||||
using namespace meshtastic;
|
using namespace meshtastic;
|
||||||
|
|
||||||
// NRF52 has AREF_VOLTAGE defined in architecture.h but
|
#ifndef AREF_VOLTAGE
|
||||||
// make sure it's included. If something is wrong with NRF52
|
#if defined(ARCH_NRF52)
|
||||||
// definition - compilation will fail on missing definition
|
/*
|
||||||
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52)
|
* Internal Reference is +/-0.6V, with an adjustable gain of 1/6, 1/5, 1/4,
|
||||||
|
* 1/3, 1/2 or 1, meaning 3.6, 3.0, 2.4, 1.8, 1.2 or 0.6V for the ADC levels.
|
||||||
|
*
|
||||||
|
* External Reference is VDD/4, with an adjustable gain of 1, 2 or 4, meaning
|
||||||
|
* VDD/4, VDD/2 or VDD for the ADC levels.
|
||||||
|
*
|
||||||
|
* Default settings are internal reference with 1/6 gain (GND..3.6V ADC range)
|
||||||
|
*/
|
||||||
|
#define AREF_VOLTAGE 3.6
|
||||||
|
#else
|
||||||
#define AREF_VOLTAGE 3.3
|
#define AREF_VOLTAGE 3.3
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this board has a battery level sensor, set this to a valid implementation
|
* If this board has a battery level sensor, set this to a valid implementation
|
||||||
@@ -227,8 +232,7 @@ static void battery_adcDisable()
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple battery level sensor that assumes the battery voltage is attached
|
* A simple battery level sensor that assumes the battery voltage is attached via a voltage-divider to an analog input
|
||||||
* via a voltage-divider to an analog input
|
|
||||||
*/
|
*/
|
||||||
class AnalogBatteryLevel : public HasBatteryLevel
|
class AnalogBatteryLevel : public HasBatteryLevel
|
||||||
{
|
{
|
||||||
@@ -306,8 +310,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
|
|
||||||
#ifndef BATTERY_SENSE_SAMPLES
|
#ifndef BATTERY_SENSE_SAMPLES
|
||||||
#define BATTERY_SENSE_SAMPLES \
|
#define BATTERY_SENSE_SAMPLES \
|
||||||
15 // Set the number of samples, it has an effect of increasing sensitivity in
|
15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment.
|
||||||
// complex electromagnetic environment.
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef BATTERY_PIN
|
#ifdef BATTERY_PIN
|
||||||
@@ -337,8 +340,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
battery_adcDisable();
|
battery_adcDisable();
|
||||||
|
|
||||||
if (!initial_read_done) {
|
if (!initial_read_done) {
|
||||||
// Flush the smoothing filter with an ADC reading, if the reading is
|
// Flush the smoothing filter with an ADC reading, if the reading is plausibly correct
|
||||||
// plausibly correct
|
|
||||||
if (scaled > last_read_value)
|
if (scaled > last_read_value)
|
||||||
last_read_value = scaled;
|
last_read_value = scaled;
|
||||||
initial_read_done = true;
|
initial_read_done = true;
|
||||||
@@ -347,8 +349,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF
|
last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF
|
||||||
}
|
}
|
||||||
|
|
||||||
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u",
|
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t)
|
||||||
// BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) (last_read_value));
|
// (last_read_value));
|
||||||
}
|
}
|
||||||
return last_read_value;
|
return last_read_value;
|
||||||
#endif // BATTERY_PIN
|
#endif // BATTERY_PIN
|
||||||
@@ -417,8 +419,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
/**
|
/**
|
||||||
* return true if there is a battery installed in this unit
|
* return true if there is a battery installed in this unit
|
||||||
*/
|
*/
|
||||||
// if we have a integrated device with a battery, we can assume that the
|
// if we have a integrated device with a battery, we can assume that the battery is always connected
|
||||||
// battery is always connected
|
|
||||||
#ifdef BATTERY_IMMUTABLE
|
#ifdef BATTERY_IMMUTABLE
|
||||||
virtual bool isBatteryConnect() override { return true; }
|
virtual bool isBatteryConnect() override { return true; }
|
||||||
#elif defined(ADC_V)
|
#elif defined(ADC_V)
|
||||||
@@ -439,10 +440,10 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
|
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// If we see a battery voltage higher than physics allows - assume charger is
|
/// If we see a battery voltage higher than physics allows - assume charger is pumping
|
||||||
/// pumping in power On some boards we don't have the power management chip
|
/// in power
|
||||||
/// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power
|
/// On some boards we don't have the power management chip (like AXPxxxx)
|
||||||
/// source
|
/// so we use EXT_PWR_DETECT GPIO pin to detect external power source
|
||||||
virtual bool isVbusIn() override
|
virtual bool isVbusIn() override
|
||||||
{
|
{
|
||||||
#ifdef EXT_PWR_DETECT
|
#ifdef EXT_PWR_DETECT
|
||||||
@@ -459,12 +460,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
}
|
}
|
||||||
// if it's not HIGH - check the battery
|
// if it's not HIGH - check the battery
|
||||||
#endif
|
#endif
|
||||||
|
#elif defined(MUZI_BASE)
|
||||||
// technically speaking this should work for all(?) NRF52 boards
|
return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk;
|
||||||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
|
||||||
// VBUS was not properly connected and detected by the CPU
|
|
||||||
#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO)
|
|
||||||
return powerHAL_isVBUSConnected();
|
|
||||||
#endif
|
#endif
|
||||||
return getBattVoltage() > chargingVolt;
|
return getBattVoltage() > chargingVolt;
|
||||||
}
|
}
|
||||||
@@ -478,18 +475,15 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse;
|
return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#if defined(ELECROW_ThinkNode_M6)
|
#ifdef EXT_CHRG_DETECT
|
||||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn();
|
|
||||||
#elif EXT_CHRG_DETECT
|
|
||||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||||
#elif defined(BATTERY_CHARGING_INV)
|
#elif defined(BATTERY_CHARGING_INV)
|
||||||
return !digitalRead(BATTERY_CHARGING_INV);
|
return !digitalRead(BATTERY_CHARGING_INV);
|
||||||
#else
|
#else
|
||||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
|
||||||
if (hasINA()) {
|
if (hasINA()) {
|
||||||
// get current flow from INA sensor - negative value means power flowing
|
// get current flow from INA sensor - negative value means power flowing into the battery
|
||||||
// into the battery default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT
|
// default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD
|
||||||
// RESISTOR <--> INA_VIN- <--> LOAD
|
|
||||||
LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address);
|
LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address);
|
||||||
#if defined(INA_CHARGING_DETECTION_INVERT)
|
#if defined(INA_CHARGING_DETECTION_INVERT)
|
||||||
return getINACurrent() > 0;
|
return getINACurrent() > 0;
|
||||||
@@ -505,8 +499,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// If we see a battery voltage higher than physics allows - assume charger is
|
/// If we see a battery voltage higher than physics allows - assume charger is pumping
|
||||||
/// pumping in power
|
/// in power
|
||||||
|
|
||||||
/// For heltecs with no battery connected, the measured voltage is 2204, so
|
/// For heltecs with no battery connected, the measured voltage is 2204, so
|
||||||
// need to be higher than that, in this case is 2500mV (3000-500)
|
// need to be higher than that, in this case is 2500mV (3000-500)
|
||||||
@@ -515,8 +509,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
|
const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
|
||||||
// Start value from minimum voltage for the filter to not start from 0
|
// Start value from minimum voltage for the filter to not start from 0
|
||||||
// that could trigger some events.
|
// that could trigger some events.
|
||||||
// This value is over-written by the first ADC reading, it the voltage seems
|
// This value is over-written by the first ADC reading, it the voltage seems reasonable.
|
||||||
// reasonable.
|
|
||||||
bool initial_read_done = false;
|
bool initial_read_done = false;
|
||||||
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
||||||
uint32_t last_read_time_ms = 0;
|
uint32_t last_read_time_ms = 0;
|
||||||
@@ -658,8 +651,7 @@ bool Power::analogInit()
|
|||||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||||
// ESP32S3
|
// ESP32S3
|
||||||
else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) {
|
else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) {
|
||||||
LOG_INFO("ADC config based on Two Point values and fitting curve "
|
LOG_INFO("ADC config based on Two Point values and fitting curve coefficients stored in eFuse");
|
||||||
"coefficients stored in eFuse");
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
else {
|
else {
|
||||||
@@ -667,7 +659,13 @@ bool Power::analogInit()
|
|||||||
}
|
}
|
||||||
#endif // ARCH_ESP32
|
#endif // ARCH_ESP32
|
||||||
|
|
||||||
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
#ifdef ARCH_NRF52
|
||||||
|
#ifdef VBAT_AR_INTERNAL
|
||||||
|
analogReference(VBAT_AR_INTERNAL);
|
||||||
|
#else
|
||||||
|
analogReference(AR_INTERNAL); // 3.6V
|
||||||
|
#endif
|
||||||
|
#endif // ARCH_NRF52
|
||||||
|
|
||||||
#ifndef ARCH_ESP32
|
#ifndef ARCH_ESP32
|
||||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||||
@@ -694,8 +692,6 @@ bool Power::setup()
|
|||||||
found = true;
|
found = true;
|
||||||
} else if (lipoChargerInit()) {
|
} else if (lipoChargerInit()) {
|
||||||
found = true;
|
found = true;
|
||||||
} else if (serialBatteryInit()) {
|
|
||||||
found = true;
|
|
||||||
} else if (meshSolarInit()) {
|
} else if (meshSolarInit()) {
|
||||||
found = true;
|
found = true;
|
||||||
} else if (analogInit()) {
|
} else if (analogInit()) {
|
||||||
@@ -722,16 +718,6 @@ bool Power::setup()
|
|||||||
runASAP = true;
|
runASAP = true;
|
||||||
},
|
},
|
||||||
CHANGE);
|
CHANGE);
|
||||||
#endif
|
|
||||||
#ifdef EXT_CHRG_DETECT
|
|
||||||
attachInterrupt(
|
|
||||||
EXT_CHRG_DETECT,
|
|
||||||
[]() {
|
|
||||||
power->setIntervalFromNow(0);
|
|
||||||
runASAP = true;
|
|
||||||
BaseType_t higherWake = 0;
|
|
||||||
},
|
|
||||||
CHANGE);
|
|
||||||
#endif
|
#endif
|
||||||
enabled = found;
|
enabled = found;
|
||||||
low_voltage_counter = 0;
|
low_voltage_counter = 0;
|
||||||
@@ -778,8 +764,7 @@ void Power::reboot()
|
|||||||
HAL_NVIC_SystemReset();
|
HAL_NVIC_SystemReset();
|
||||||
#else
|
#else
|
||||||
rebootAtMsec = -1;
|
rebootAtMsec = -1;
|
||||||
LOG_WARN("FIXME implement reboot for this platform. Note that some settings "
|
LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied");
|
||||||
"require a restart to be applied");
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -789,12 +774,9 @@ void Power::shutdown()
|
|||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
if (screen) {
|
if (screen) {
|
||||||
#ifdef T_DECK_PRO
|
#ifdef T_DECK_PRO
|
||||||
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!",
|
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button
|
||||||
0); // T-Deck Pro has no power button
|
|
||||||
#elif defined(USE_EINK)
|
#elif defined(USE_EINK)
|
||||||
screen->showSimpleBanner("Shutting Down...",
|
screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen
|
||||||
2250); // dismiss after 3 seconds to avoid the
|
|
||||||
// banner on the sleep screen
|
|
||||||
#else
|
#else
|
||||||
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
|
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
|
||||||
#endif
|
#endif
|
||||||
@@ -804,9 +786,7 @@ void Power::shutdown()
|
|||||||
playShutdownMelody();
|
playShutdownMelody();
|
||||||
#endif
|
#endif
|
||||||
nodeDB->saveToDisk();
|
nodeDB->saveToDisk();
|
||||||
#if HAS_SCREEN
|
|
||||||
messageStore.saveToFlash();
|
|
||||||
#endif
|
|
||||||
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||||
#ifdef PIN_LED1
|
#ifdef PIN_LED1
|
||||||
ledOff(PIN_LED1);
|
ledOff(PIN_LED1);
|
||||||
@@ -816,9 +796,6 @@ void Power::shutdown()
|
|||||||
#endif
|
#endif
|
||||||
#ifdef PIN_LED3
|
#ifdef PIN_LED3
|
||||||
ledOff(PIN_LED3);
|
ledOff(PIN_LED3);
|
||||||
#endif
|
|
||||||
#ifdef LED_NOTIFICATION
|
|
||||||
ledOff(LED_NOTIFICATION);
|
|
||||||
#endif
|
#endif
|
||||||
doDeepSleep(DELAY_FOREVER, true, true);
|
doDeepSleep(DELAY_FOREVER, true, true);
|
||||||
#elif defined(ARCH_PORTDUINO)
|
#elif defined(ARCH_PORTDUINO)
|
||||||
@@ -836,8 +813,7 @@ void Power::readPowerStatus()
|
|||||||
int32_t batteryVoltageMv = -1; // Assume unknown
|
int32_t batteryVoltageMv = -1; // Assume unknown
|
||||||
int8_t batteryChargePercent = -1;
|
int8_t batteryChargePercent = -1;
|
||||||
OptionalBool usbPowered = OptUnknown;
|
OptionalBool usbPowered = OptUnknown;
|
||||||
OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM
|
OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM code doesn't run every time
|
||||||
// code doesn't run every time
|
|
||||||
OptionalBool isChargingNow = OptUnknown;
|
OptionalBool isChargingNow = OptUnknown;
|
||||||
|
|
||||||
if (batteryLevel) {
|
if (batteryLevel) {
|
||||||
@@ -850,10 +826,9 @@ void Power::readPowerStatus()
|
|||||||
if (batteryLevel->getBatteryPercent() >= 0) {
|
if (batteryLevel->getBatteryPercent() >= 0) {
|
||||||
batteryChargePercent = batteryLevel->getBatteryPercent();
|
batteryChargePercent = batteryLevel->getBatteryPercent();
|
||||||
} else {
|
} else {
|
||||||
// If the AXP192 returns a percentage less than 0, the feature is either
|
// If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error
|
||||||
// not supported or there is an error In that case, we compute an
|
// In that case, we compute an estimate of the charge percent based on open circuit voltage table defined
|
||||||
// estimate of the charge percent based on open circuit voltage table
|
// in power.h
|
||||||
// defined in power.h
|
|
||||||
batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) /
|
batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) /
|
||||||
((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))),
|
((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))),
|
||||||
0, 100);
|
0, 100);
|
||||||
@@ -861,12 +836,12 @@ void Power::readPowerStatus()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way
|
// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way better instead to make a Nrf52IsUsbPowered subclass
|
||||||
// better instead to make a Nrf52IsUsbPowered subclass (which shares a
|
// (which shares a superclass with the BatteryLevel stuff)
|
||||||
// superclass with the BatteryLevel stuff) that just provides a few methods. But
|
// that just provides a few methods. But in the interest of fixing this bug I'm going to follow current
|
||||||
// in the interest of fixing this bug I'm going to follow current practice.
|
// practice.
|
||||||
#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates
|
#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates the power states. Takes 20 seconds or so to detect
|
||||||
// the power states. Takes 20 seconds or so to detect changes.
|
// changes.
|
||||||
|
|
||||||
nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get();
|
nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get();
|
||||||
// LOG_DEBUG("NRF Power %d", nrf_usb_state);
|
// LOG_DEBUG("NRF Power %d", nrf_usb_state);
|
||||||
@@ -940,9 +915,8 @@ void Power::readPowerStatus()
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// If we have a battery at all and it is less than 0%, force deep sleep if we
|
// If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in
|
||||||
// have more than 10 low readings in a row. NOTE: min LiIon/LiPo voltage
|
// a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
|
||||||
// is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
|
|
||||||
//
|
//
|
||||||
|
|
||||||
if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) {
|
if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) {
|
||||||
@@ -964,8 +938,8 @@ int32_t Power::runOnce()
|
|||||||
readPowerStatus();
|
readPowerStatus();
|
||||||
|
|
||||||
#ifdef HAS_PMU
|
#ifdef HAS_PMU
|
||||||
// WE no longer use the IRQ line to wake the CPU (due to false wakes from
|
// WE no longer use the IRQ line to wake the CPU (due to false wakes from sleep), but we do poll
|
||||||
// sleep), but we do poll the IRQ status by reading the registers over I2C
|
// the IRQ status by reading the registers over I2C
|
||||||
if (PMU) {
|
if (PMU) {
|
||||||
|
|
||||||
PMU->getIrqStatus();
|
PMU->getIrqStatus();
|
||||||
@@ -1007,8 +981,7 @@ int32_t Power::runOnce()
|
|||||||
PMU->clearIrqStatus();
|
PMU->clearIrqStatus();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
// Only read once every 20 seconds once the power status for the app has been
|
// Only read once every 20 seconds once the power status for the app has been initialized
|
||||||
// initialized
|
|
||||||
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
|
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1016,12 +989,10 @@ int32_t Power::runOnce()
|
|||||||
* Init the power manager chip
|
* Init the power manager chip
|
||||||
*
|
*
|
||||||
* axp192 power
|
* axp192 power
|
||||||
DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose
|
DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms to the axp192 because the OLED and the
|
||||||
comms to the axp192 because the OLED and the axp192 share the same i2c bus,
|
axp192 share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this
|
||||||
instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max ->
|
on!) LDO1 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for a couple of
|
||||||
ESP32 (keep this on!) LDO1 30mA -> charges GPS backup battery // charges the
|
days), can not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
|
||||||
tiny J13 battery by the GPS to power the GPS ram (for a couple of days), can
|
|
||||||
not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
|
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
bool Power::axpChipInit()
|
bool Power::axpChipInit()
|
||||||
@@ -1066,10 +1037,9 @@ bool Power::axpChipInit()
|
|||||||
|
|
||||||
if (!PMU) {
|
if (!PMU) {
|
||||||
/*
|
/*
|
||||||
* In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will
|
* In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will be called at the same time.
|
||||||
* be called at the same time. In order not to affect other devices, if the
|
* In order not to affect other devices, if the initialization of the PMU fails, Wire needs to be re-initialized once,
|
||||||
* initialization of the PMU fails, Wire needs to be re-initialized once, if
|
* if there are multiple devices sharing the bus.
|
||||||
* there are multiple devices sharing the bus.
|
|
||||||
* * */
|
* * */
|
||||||
#ifndef PMU_USE_WIRE1
|
#ifndef PMU_USE_WIRE1
|
||||||
w->begin(I2C_SDA, I2C_SCL);
|
w->begin(I2C_SDA, I2C_SCL);
|
||||||
@@ -1086,8 +1056,8 @@ bool Power::axpChipInit()
|
|||||||
PMU->enablePowerOutput(XPOWERS_LDO2);
|
PMU->enablePowerOutput(XPOWERS_LDO2);
|
||||||
|
|
||||||
// oled module power channel,
|
// oled module power channel,
|
||||||
// disable it will cause abnormal communication between boot and AXP power
|
// disable it will cause abnormal communication between boot and AXP power supply,
|
||||||
// supply, do not turn it off
|
// do not turn it off
|
||||||
PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
|
PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
|
||||||
// enable oled power
|
// enable oled power
|
||||||
PMU->enablePowerOutput(XPOWERS_DCDC1);
|
PMU->enablePowerOutput(XPOWERS_DCDC1);
|
||||||
@@ -1114,8 +1084,7 @@ bool Power::axpChipInit()
|
|||||||
PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
|
PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
|
||||||
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
||||||
|
|
||||||
/*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it
|
/*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it uses an AXP2101 power chip*/
|
||||||
* uses an AXP2101 power chip*/
|
|
||||||
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
|
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
|
||||||
// Unuse power channel
|
// Unuse power channel
|
||||||
PMU->disablePowerOutput(XPOWERS_DCDC2);
|
PMU->disablePowerOutput(XPOWERS_DCDC2);
|
||||||
@@ -1150,8 +1119,8 @@ bool Power::axpChipInit()
|
|||||||
// t-beam s3 core
|
// t-beam s3 core
|
||||||
/**
|
/**
|
||||||
* gnss module power channel
|
* gnss module power channel
|
||||||
* The default ALDO4 is off, you need to turn on the GNSS power first,
|
* The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during
|
||||||
* otherwise it will be invalid during initialization
|
* initialization
|
||||||
*/
|
*/
|
||||||
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
|
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
|
||||||
PMU->enablePowerOutput(XPOWERS_ALDO4);
|
PMU->enablePowerOutput(XPOWERS_ALDO4);
|
||||||
@@ -1177,11 +1146,11 @@ bool Power::axpChipInit()
|
|||||||
PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300);
|
PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300);
|
||||||
PMU->enablePowerOutput(XPOWERS_ALDO1);
|
PMU->enablePowerOutput(XPOWERS_ALDO1);
|
||||||
|
|
||||||
// sdcard (T-Beam S3) / gnns (T-Watch S3 Plus) power channel
|
// sdcard power channel
|
||||||
PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
|
PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
|
||||||
#ifndef T_WATCH_S3
|
|
||||||
PMU->enablePowerOutput(XPOWERS_BLDO1);
|
PMU->enablePowerOutput(XPOWERS_BLDO1);
|
||||||
#else
|
|
||||||
|
#ifdef T_WATCH_S3
|
||||||
// DRV2605 power channel
|
// DRV2605 power channel
|
||||||
PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300);
|
PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300);
|
||||||
PMU->enablePowerOutput(XPOWERS_BLDO2);
|
PMU->enablePowerOutput(XPOWERS_BLDO2);
|
||||||
@@ -1201,8 +1170,7 @@ bool Power::axpChipInit()
|
|||||||
// disable all axp chip interrupt
|
// disable all axp chip interrupt
|
||||||
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
|
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
|
||||||
|
|
||||||
// Set the constant current charging current of AXP2101, temporarily use
|
// Set the constant current charging current of AXP2101, temporarily use 500mA by default
|
||||||
// 500mA by default
|
|
||||||
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
|
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
|
||||||
|
|
||||||
// Set up the charging voltage
|
// Set up the charging voltage
|
||||||
@@ -1268,12 +1236,11 @@ bool Power::axpChipInit()
|
|||||||
PMU->getPowerChannelVoltage(XPOWERS_BLDO2));
|
PMU->getPowerChannelVoltage(XPOWERS_BLDO2));
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can safely ignore this approach for most (or all) boards because MCU
|
// We can safely ignore this approach for most (or all) boards because MCU turned off
|
||||||
// turned off earlier than battery discharged to 2.6V.
|
// earlier than battery discharged to 2.6V.
|
||||||
//
|
//
|
||||||
// Unfortunately for now we can't use this killswitch for RAK4630-based boards
|
// Unfortanly for now we can't use this killswitch for RAK4630-based boards because they have a bug with
|
||||||
// because they have a bug with battery voltage measurement. Probably it
|
// battery voltage measurement. Probably it sometimes drops to low values.
|
||||||
// sometimes drops to low values.
|
|
||||||
#ifndef RAK4630
|
#ifndef RAK4630
|
||||||
// Set PMU shutdown voltage at 2.6V to maximize battery utilization
|
// Set PMU shutdown voltage at 2.6V to maximize battery utilization
|
||||||
PMU->setSysPowerDownVoltage(2600);
|
PMU->setSysPowerDownVoltage(2600);
|
||||||
@@ -1292,12 +1259,10 @@ bool Power::axpChipInit()
|
|||||||
attachInterrupt(
|
attachInterrupt(
|
||||||
PMU_IRQ, [] { pmu_irq = true; }, FALLING);
|
PMU_IRQ, [] { pmu_irq = true; }, FALLING);
|
||||||
|
|
||||||
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ
|
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ because it occurs repeatedly while there is
|
||||||
// because it occurs repeatedly while there is no battery also it could cause
|
// no battery also it could cause inadvertent waking from light sleep just because the battery filled
|
||||||
// inadvertent waking from light sleep just because the battery filled we
|
// we don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while no battery installed
|
||||||
// don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while
|
// we don't look at AXPXXX_VBUS_REMOVED_IRQ because we don't have anything hooked to vbus
|
||||||
// no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we
|
|
||||||
// don't have anything hooked to vbus
|
|
||||||
PMU->enableIRQ(pmuIrqMask);
|
PMU->enableIRQ(pmuIrqMask);
|
||||||
|
|
||||||
PMU->clearIrqStatus();
|
PMU->clearIrqStatus();
|
||||||
@@ -1413,8 +1378,8 @@ class LipoCharger : public HasBatteryLevel
|
|||||||
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
|
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
|
||||||
if (result) {
|
if (result) {
|
||||||
LOG_INFO("PPM BQ25896 init succeeded");
|
LOG_INFO("PPM BQ25896 init succeeded");
|
||||||
// Set the minimum operating voltage. Below this voltage, the PPM will
|
// Set the minimum operating voltage. Below this voltage, the PPM will protect
|
||||||
// protect PPM->setSysPowerDownVoltage(3100);
|
// PPM->setSysPowerDownVoltage(3100);
|
||||||
|
|
||||||
// Set input current limit, default is 500mA
|
// Set input current limit, default is 500mA
|
||||||
// PPM->setInputCurrentLimit(800);
|
// PPM->setInputCurrentLimit(800);
|
||||||
@@ -1437,8 +1402,7 @@ class LipoCharger : public HasBatteryLevel
|
|||||||
PPM->enableMeasure();
|
PPM->enableMeasure();
|
||||||
|
|
||||||
// Turn on charging function
|
// Turn on charging function
|
||||||
// If there is no battery connected, do not turn on the charging
|
// If there is no battery connected, do not turn on the charging function
|
||||||
// function
|
|
||||||
PPM->enableCharge();
|
PPM->enableCharge();
|
||||||
} else {
|
} else {
|
||||||
LOG_WARN("PPM BQ25896 init failed");
|
LOG_WARN("PPM BQ25896 init failed");
|
||||||
@@ -1473,8 +1437,7 @@ class LipoCharger : public HasBatteryLevel
|
|||||||
virtual int getBatteryPercent() override
|
virtual int getBatteryPercent() override
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
// return bq->getChargePercent(); // don't use BQ27220 for battery percent,
|
// return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated
|
||||||
// it is not calibrated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1596,143 +1559,10 @@ bool Power::meshSolarInit()
|
|||||||
|
|
||||||
#else
|
#else
|
||||||
/**
|
/**
|
||||||
* The meshSolar battery level sensor is unavailable - default to
|
* The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||||
* AnalogBatteryLevel
|
|
||||||
*/
|
*/
|
||||||
bool Power::meshSolarInit()
|
bool Power::meshSolarInit()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAS_SERIAL_BATTERY_LEVEL
|
|
||||||
#include <SoftwareSerial.h>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SerialBatteryLevel class for pulling battery information from a secondary MCU over serial.
|
|
||||||
*/
|
|
||||||
class SerialBatteryLevel : public HasBatteryLevel
|
|
||||||
{
|
|
||||||
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* Init the I2C meshSolar battery level sensor
|
|
||||||
*/
|
|
||||||
bool runOnce()
|
|
||||||
{
|
|
||||||
BatterySerial.begin(4800);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
|
||||||
*/
|
|
||||||
virtual int getBatteryPercent() override { return v_percent; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The raw voltage of the battery in millivolts, or NAN if unknown
|
|
||||||
*/
|
|
||||||
virtual uint16_t getBattVoltage() override { return voltage * 1000; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return true if there is a battery installed in this unit
|
|
||||||
*/
|
|
||||||
virtual bool isBatteryConnect() override
|
|
||||||
{
|
|
||||||
// definitely need to gobble up more bytes at once
|
|
||||||
if (BatterySerial.available() > 5) {
|
|
||||||
// LOG_WARN("SerialBatteryLevel: %u bytes available", BatterySerial.available());
|
|
||||||
while (BatterySerial.available() > 11) {
|
|
||||||
BatterySerial.read(); // flush old data
|
|
||||||
}
|
|
||||||
// LOG_WARN("SerialBatteryLevel: %u bytes now available", BatterySerial.available());
|
|
||||||
int tries = 0;
|
|
||||||
while (BatterySerial.read() != 0xFE) {
|
|
||||||
tries++; // wait for start byte
|
|
||||||
if (tries > 10) {
|
|
||||||
LOG_WARN("SerialBatteryLevel: no start byte found");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Data[1] = BatterySerial.read();
|
|
||||||
Data[2] = BatterySerial.read();
|
|
||||||
Data[3] = BatterySerial.read();
|
|
||||||
Data[4] = BatterySerial.read();
|
|
||||||
Data[5] = BatterySerial.read();
|
|
||||||
if (Data[5] != 0xFD) {
|
|
||||||
LOG_WARN("SerialBatteryLevel: invalid end byte %02x", Data[5]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
v_percent = Data[1];
|
|
||||||
voltage = Data[2] + (((float)Data[3]) / 100) + (((float)Data[4]) / 10000);
|
|
||||||
voltage *= 2;
|
|
||||||
// LOG_WARN("SerialBatteryLevel: received data %u, %f, %02x", v_percent, voltage, Data[5]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// This function runs first, so use it to grab the latest data from the secondary MCU
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return true if there is an external power source detected
|
|
||||||
*/
|
|
||||||
virtual bool isVbusIn() override
|
|
||||||
{
|
|
||||||
#if defined(EXT_CHRG_DETECT)
|
|
||||||
|
|
||||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual bool isCharging() override
|
|
||||||
{
|
|
||||||
#ifdef EXT_CHRG_DETECT
|
|
||||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
// by default, we check the battery voltage only
|
|
||||||
return isVbusIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX);
|
|
||||||
uint8_t Data[6] = {0};
|
|
||||||
int v_percent = 0;
|
|
||||||
float voltage = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
SerialBatteryLevel serialBatteryLevel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init the serial battery level sensor
|
|
||||||
*/
|
|
||||||
bool Power::serialBatteryInit()
|
|
||||||
{
|
|
||||||
#ifdef EXT_PWR_DETECT
|
|
||||||
pinMode(EXT_PWR_DETECT, INPUT);
|
|
||||||
#endif
|
|
||||||
#ifdef EXT_CHRG_DETECT
|
|
||||||
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
bool result = serialBatteryLevel.runOnce();
|
|
||||||
LOG_DEBUG("Power::serialBatteryInit serial battery sensor is %s", result ? "ready" : "not ready yet");
|
|
||||||
if (!result)
|
|
||||||
return false;
|
|
||||||
batteryLevel = &serialBatteryLevel;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
|
||||||
/**
|
|
||||||
* If this device has no serial battery level sensor, don't try to use it.
|
|
||||||
*/
|
|
||||||
bool Power::serialBatteryInit()
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if HAS_NETWORKING
|
#if HAS_NETWORKING
|
||||||
extern meshtastic::Syslog syslog;
|
extern Syslog syslog;
|
||||||
#endif
|
#endif
|
||||||
void RedirectablePrint::rpInit()
|
void RedirectablePrint::rpInit()
|
||||||
{
|
{
|
||||||
@@ -131,7 +131,6 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
|
|||||||
int hour = hms / SEC_PER_HOUR;
|
int hour = hms / SEC_PER_HOUR;
|
||||||
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||||
|
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
::printf("%s ", logLevel);
|
::printf("%s ", logLevel);
|
||||||
if (color) {
|
if (color) {
|
||||||
|
|||||||
+10
-2
@@ -54,7 +54,7 @@ size_t SafeFile::write(const uint8_t *buffer, size_t size)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Atomically close the file (overwriting any old version) and readback the contents to confirm the hash matches
|
* Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches
|
||||||
*
|
*
|
||||||
* @return false for failure
|
* @return false for failure
|
||||||
*/
|
*/
|
||||||
@@ -73,7 +73,15 @@ bool SafeFile::close()
|
|||||||
if (!testReadback())
|
if (!testReadback())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Rename or overwrite (atomic operation)
|
{ // Scope for lock
|
||||||
|
concurrency::LockGuard g(spiLock);
|
||||||
|
// brief window of risk here ;-)
|
||||||
|
if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) {
|
||||||
|
LOG_ERROR("Can't remove old pref file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String filenameTmp = filename;
|
String filenameTmp = filename;
|
||||||
filenameTmp += ".tmp";
|
filenameTmp += ".tmp";
|
||||||
if (!renameFile(filenameTmp.c_str(), filename.c_str())) {
|
if (!renameFile(filenameTmp.c_str(), filename.c_str())) {
|
||||||
|
|||||||
@@ -22,19 +22,12 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
|
|||||||
|
|
||||||
// Handle different input events with appropriate buzzer feedback
|
// Handle different input events with appropriate buzzer feedback
|
||||||
switch (event->inputEvent) {
|
switch (event->inputEvent) {
|
||||||
#ifdef INPUTDRIVER_ENCODER_TYPE
|
|
||||||
case INPUT_BROKER_SELECT:
|
|
||||||
case INPUT_BROKER_SELECT_LONG:
|
|
||||||
playClick();
|
|
||||||
break;
|
|
||||||
#else
|
|
||||||
case INPUT_BROKER_USER_PRESS:
|
case INPUT_BROKER_USER_PRESS:
|
||||||
case INPUT_BROKER_ALT_PRESS:
|
case INPUT_BROKER_ALT_PRESS:
|
||||||
case INPUT_BROKER_SELECT:
|
case INPUT_BROKER_SELECT:
|
||||||
case INPUT_BROKER_SELECT_LONG:
|
case INPUT_BROKER_SELECT_LONG:
|
||||||
playBeep();
|
playBeep(); // Confirmation feedback
|
||||||
break;
|
break;
|
||||||
#endif
|
|
||||||
|
|
||||||
case INPUT_BROKER_UP:
|
case INPUT_BROKER_UP:
|
||||||
case INPUT_BROKER_UP_LONG:
|
case INPUT_BROKER_UP_LONG:
|
||||||
@@ -65,4 +58,4 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return 0; // Allow other handlers to process the event
|
return 0; // Allow other handlers to process the event
|
||||||
}
|
}
|
||||||
+2
-31
@@ -35,14 +35,6 @@ struct ToneDuration {
|
|||||||
#define NOTE_G6 1568
|
#define NOTE_G6 1568
|
||||||
#define NOTE_E7 2637
|
#define NOTE_E7 2637
|
||||||
|
|
||||||
#define NOTE_C4 262
|
|
||||||
#define NOTE_E4 330
|
|
||||||
#define NOTE_G4 392
|
|
||||||
#define NOTE_A4 440
|
|
||||||
#define NOTE_C5 523
|
|
||||||
#define NOTE_E5 659
|
|
||||||
#define NOTE_G5 784
|
|
||||||
|
|
||||||
const int DURATION_1_16 = 62; // 1/16 note
|
const int DURATION_1_16 = 62; // 1/16 note
|
||||||
const int DURATION_1_8 = 125; // 1/8 note
|
const int DURATION_1_8 = 125; // 1/8 note
|
||||||
const int DURATION_1_4 = 250; // 1/4 note
|
const int DURATION_1_4 = 250; // 1/4 note
|
||||||
@@ -73,7 +65,7 @@ void playTones(const ToneDuration *tone_durations, int size)
|
|||||||
|
|
||||||
void playBeep()
|
void playBeep()
|
||||||
{
|
{
|
||||||
ToneDuration melody[] = {{NOTE_B3, DURATION_1_16}};
|
ToneDuration melody[] = {{NOTE_B3, DURATION_1_8}};
|
||||||
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,14 +113,7 @@ void playShutdownMelody()
|
|||||||
void playChirp()
|
void playChirp()
|
||||||
{
|
{
|
||||||
// A short, friendly "chirp" sound for key presses
|
// A short, friendly "chirp" sound for key presses
|
||||||
ToneDuration melody[] = {{NOTE_AS3, 20}}; // Short AS3 note
|
ToneDuration melody[] = {{NOTE_AS3, 20}}; // Very short AS3 note
|
||||||
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
|
||||||
}
|
|
||||||
|
|
||||||
void playClick()
|
|
||||||
{
|
|
||||||
// A very short "click" sound with minimum delay; ideal for rotary encoder events
|
|
||||||
ToneDuration melody[] = {{NOTE_AS3, 1}}; // Very Short AS3
|
|
||||||
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,17 +182,3 @@ void playComboTune()
|
|||||||
};
|
};
|
||||||
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
||||||
}
|
}
|
||||||
|
|
||||||
void play4ClickDown()
|
|
||||||
{
|
|
||||||
ToneDuration melody[] = {{NOTE_G5, 55}, {NOTE_E5, 55}, {NOTE_C5, 60}, {NOTE_A4, 55}, {NOTE_G4, 55},
|
|
||||||
{NOTE_E4, 65}, {NOTE_C4, 80}, {NOTE_G3, 120}, {NOTE_E3, 160}, {NOTE_SILENT, 120}};
|
|
||||||
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
|
||||||
}
|
|
||||||
|
|
||||||
void play4ClickUp()
|
|
||||||
{
|
|
||||||
// Quick high-pitched notes with trills
|
|
||||||
ToneDuration melody[] = {{NOTE_F5, 50}, {NOTE_G6, 45}, {NOTE_E7, 60}};
|
|
||||||
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
|
|
||||||
}
|
|
||||||
@@ -7,11 +7,8 @@ void playShutdownMelody();
|
|||||||
void playGPSEnableBeep();
|
void playGPSEnableBeep();
|
||||||
void playGPSDisableBeep();
|
void playGPSDisableBeep();
|
||||||
void playComboTune();
|
void playComboTune();
|
||||||
void play4ClickDown();
|
|
||||||
void play4ClickUp();
|
|
||||||
void playBoop();
|
void playBoop();
|
||||||
void playChirp();
|
void playChirp();
|
||||||
void playClick();
|
|
||||||
void playLongPressLeadUp();
|
void playLongPressLeadUp();
|
||||||
bool playNextLeadUpNote(); // Play the next note in the lead-up sequence
|
bool playNextLeadUpNote(); // Play the next note in the lead-up sequence
|
||||||
void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning
|
void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning
|
||||||
+13
-21
@@ -29,8 +29,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#if __has_include("Melopero_RV3028.h")
|
#if __has_include("Melopero_RV3028.h")
|
||||||
#include "Melopero_RV3028.h"
|
#include "Melopero_RV3028.h"
|
||||||
#endif
|
#endif
|
||||||
#if __has_include("SensorRtcHelper.hpp")
|
#if __has_include("pcf8563.h")
|
||||||
#include "SensorRtcHelper.hpp"
|
#include "pcf8563.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Offer chance for variant-specific defines */
|
/* Offer chance for variant-specific defines */
|
||||||
@@ -155,10 +155,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Default system gain to 0 if not defined
|
// Default system gain to 0 if not defined
|
||||||
#ifndef NUM_PA_POINTS
|
|
||||||
#define NUM_PA_POINTS 1
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef TX_GAIN_LORA
|
#ifndef TX_GAIN_LORA
|
||||||
#define TX_GAIN_LORA 0
|
#define TX_GAIN_LORA 0
|
||||||
#endif
|
#endif
|
||||||
@@ -176,12 +172,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// OLED & Input
|
// OLED & Input
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
#define SSD1306_ADDRESS_L 0x3C // Addr = 0
|
|
||||||
#define SSD1306_ADDRESS_H 0x3D // Addr = 1
|
|
||||||
|
|
||||||
#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK)
|
#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK)
|
||||||
#define SSD1306_ADDRESS SSD1306_ADDRESS_H
|
#define SSD1306_ADDRESS 0x3D
|
||||||
#define USE_SH1106
|
#define USE_SH1106
|
||||||
|
#else
|
||||||
|
#define SSD1306_ADDRESS 0x3C
|
||||||
#endif
|
#endif
|
||||||
#define ST7567_ADDRESS 0x3F
|
#define ST7567_ADDRESS 0x3F
|
||||||
|
|
||||||
@@ -210,7 +205,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define INA_ADDR_WAVESHARE_UPS 0x43
|
#define INA_ADDR_WAVESHARE_UPS 0x43
|
||||||
#define INA3221_ADDR 0x42
|
#define INA3221_ADDR 0x42
|
||||||
#define MAX1704X_ADDR 0x36
|
#define MAX1704X_ADDR 0x36
|
||||||
#define QMC6310U_ADDR 0x1C
|
#define QMC6310_ADDR 0x1C
|
||||||
#define QMI8658_ADDR 0x6B
|
#define QMI8658_ADDR 0x6B
|
||||||
#define QMC5883L_ADDR 0x0D
|
#define QMC5883L_ADDR 0x0D
|
||||||
#define HMC5883L_ADDR 0x1E
|
#define HMC5883L_ADDR 0x1E
|
||||||
@@ -219,7 +214,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define LPS22HB_ADDR_ALT 0x5D
|
#define LPS22HB_ADDR_ALT 0x5D
|
||||||
#define SHT31_4x_ADDR 0x44
|
#define SHT31_4x_ADDR 0x44
|
||||||
#define SHT31_4x_ADDR_ALT 0x45
|
#define SHT31_4x_ADDR_ALT 0x45
|
||||||
#define PMSA003I_ADDR 0x12
|
#define PMSA0031_ADDR 0x12
|
||||||
#define QMA6100P_ADDR 0x12
|
#define QMA6100P_ADDR 0x12
|
||||||
#define AHT10_ADDR 0x38
|
#define AHT10_ADDR 0x38
|
||||||
#define RCWL9620_ADDR 0x57
|
#define RCWL9620_ADDR 0x57
|
||||||
@@ -241,7 +236,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
|
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
|
||||||
#define BQ25896_ADDR 0x6B
|
#define BQ25896_ADDR 0x6B
|
||||||
#define LTR553ALS_ADDR 0x23
|
#define LTR553ALS_ADDR 0x23
|
||||||
#define SEN5X_ADDR 0x69
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// ACCELEROMETER
|
// ACCELEROMETER
|
||||||
@@ -391,6 +385,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#ifndef HAS_RADIO
|
#ifndef HAS_RADIO
|
||||||
#define HAS_RADIO 0
|
#define HAS_RADIO 0
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef HAS_RTC
|
||||||
|
#define HAS_RTC 0
|
||||||
|
#endif
|
||||||
#ifndef HAS_CPU_SHUTDOWN
|
#ifndef HAS_CPU_SHUTDOWN
|
||||||
#define HAS_CPU_SHUTDOWN 0
|
#define HAS_CPU_SHUTDOWN 0
|
||||||
#endif
|
#endif
|
||||||
@@ -426,16 +423,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define HAS_RGB_LED
|
#define HAS_RGB_LED
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef LED_STATE_OFF
|
||||||
|
#define LED_STATE_OFF 0
|
||||||
|
#endif
|
||||||
#ifndef LED_STATE_ON
|
#ifndef LED_STATE_ON
|
||||||
#define LED_STATE_ON 1
|
#define LED_STATE_ON 1
|
||||||
#endif
|
#endif
|
||||||
#ifndef LED_STATE_OFF
|
|
||||||
#define LED_STATE_OFF (LED_STATE_ON ^ 1)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef ledOff
|
|
||||||
#define ledOff(pin) pinMode(pin, INPUT)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// default mapping of pins
|
// default mapping of pins
|
||||||
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)
|
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)
|
||||||
@@ -475,7 +468,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define MESHTASTIC_EXCLUDE_AUDIO 1
|
#define MESHTASTIC_EXCLUDE_AUDIO 1
|
||||||
#define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1
|
#define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1
|
||||||
#define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1
|
#define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1
|
||||||
#define MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR 1
|
|
||||||
#define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1
|
#define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1
|
||||||
#define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1
|
#define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1
|
||||||
#define MESHTASTIC_EXCLUDE_PAXCOUNTER 1
|
#define MESHTASTIC_EXCLUDE_PAXCOUNTER 1
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ ScanI2C::FoundDevice ScanI2C::firstScreen() const
|
|||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstRTC() const
|
ScanI2C::FoundDevice ScanI2C::firstRTC() const
|
||||||
{
|
{
|
||||||
ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_PCF85063, RTC_RX8130CE};
|
ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_RX8130CE};
|
||||||
return firstOfOrNONE(4, types);
|
return firstOfOrNONE(3, types);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
|
ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
|
||||||
@@ -43,7 +43,7 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
|||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
||||||
{
|
{
|
||||||
ScanI2C::DeviceType types[] = {PMSA003I, SCD4X};
|
ScanI2C::DeviceType types[] = {PMSA0031, SCD4X};
|
||||||
return firstOfOrNONE(2, types);
|
return firstOfOrNONE(2, types);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class ScanI2C
|
|||||||
SCREEN_ST7567,
|
SCREEN_ST7567,
|
||||||
RTC_RV3028,
|
RTC_RV3028,
|
||||||
RTC_PCF8563,
|
RTC_PCF8563,
|
||||||
RTC_PCF85063,
|
|
||||||
RTC_RX8130CE,
|
RTC_RX8130CE,
|
||||||
CARDKB,
|
CARDKB,
|
||||||
TDECKKB,
|
TDECKKB,
|
||||||
@@ -35,12 +34,11 @@ class ScanI2C
|
|||||||
SHT4X,
|
SHT4X,
|
||||||
SHTC3,
|
SHTC3,
|
||||||
LPS22HB,
|
LPS22HB,
|
||||||
QMC6310U,
|
QMC6310,
|
||||||
QMC6310N,
|
|
||||||
QMI8658,
|
QMI8658,
|
||||||
QMC5883L,
|
QMC5883L,
|
||||||
HMC5883L,
|
HMC5883L,
|
||||||
PMSA003I,
|
PMSA0031,
|
||||||
QMA6100P,
|
QMA6100P,
|
||||||
MPU6050,
|
MPU6050,
|
||||||
LIS3DH,
|
LIS3DH,
|
||||||
@@ -88,8 +86,7 @@ class ScanI2C
|
|||||||
BH1750,
|
BH1750,
|
||||||
DA217,
|
DA217,
|
||||||
CHSC6X,
|
CHSC6X,
|
||||||
CST226SE,
|
CST226SE
|
||||||
SEN5X
|
|
||||||
} DeviceType;
|
} DeviceType;
|
||||||
|
|
||||||
// typedef uint8_t DeviceAddress;
|
// typedef uint8_t DeviceAddress;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
#endif
|
#endif
|
||||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
|
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
|
||||||
#include "meshUtils.h" // vformat
|
#include "meshUtils.h" // vformat
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool in_array(uint8_t *array, int size, uint8_t lookfor)
|
bool in_array(uint8_t *array, int size, uint8_t lookfor)
|
||||||
@@ -64,16 +63,12 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const
|
|||||||
if (i2cBus->available()) {
|
if (i2cBus->available()) {
|
||||||
r = i2cBus->read();
|
r = i2cBus->read();
|
||||||
}
|
}
|
||||||
if (r == 0x80) {
|
|
||||||
LOG_INFO("QMC6310N found at address 0x%02X", addr.address);
|
|
||||||
return ScanI2C::DeviceType::QMC6310N;
|
|
||||||
}
|
|
||||||
r &= 0x0f;
|
r &= 0x0f;
|
||||||
|
|
||||||
if (r == 0x08 || r == 0x00) {
|
if (r == 0x08 || r == 0x00) {
|
||||||
logFoundDevice("SH1106", (uint8_t)addr.address);
|
logFoundDevice("SH1106", (uint8_t)addr.address);
|
||||||
o_probe = SCREEN_SH1106; // SH1106
|
o_probe = SCREEN_SH1106; // SH1106
|
||||||
} else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07 || r == 0x05) {
|
} else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07) {
|
||||||
logFoundDevice("SSD1306", (uint8_t)addr.address);
|
logFoundDevice("SSD1306", (uint8_t)addr.address);
|
||||||
o_probe = SCREEN_SSD1306; // SSD1306
|
o_probe = SCREEN_SSD1306; // SSD1306
|
||||||
}
|
}
|
||||||
@@ -111,49 +106,10 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
|
|||||||
if (i2cBus->available())
|
if (i2cBus->available())
|
||||||
i2cBus->read();
|
i2cBus->read();
|
||||||
}
|
}
|
||||||
LOG_DEBUG("Register value from 0x%x: 0x%x", registerLocation.i2cAddress.address, value);
|
LOG_DEBUG("Register value: 0x%x", value);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// for SEN5X detection
|
|
||||||
// Note, this code needs to be called before setting the I2C bus speed
|
|
||||||
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
|
|
||||||
// detection will not work
|
|
||||||
String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
|
|
||||||
{
|
|
||||||
uint8_t cmd[] = {0xD0, 0x14};
|
|
||||||
uint8_t response[48] = {0};
|
|
||||||
|
|
||||||
i2cBus->beginTransmission(address);
|
|
||||||
i2cBus->write(cmd, 2);
|
|
||||||
if (i2cBus->endTransmission() != 0)
|
|
||||||
return "";
|
|
||||||
|
|
||||||
delay(20);
|
|
||||||
if (i2cBus->requestFrom(address, (uint8_t)48) != 48)
|
|
||||||
return "";
|
|
||||||
|
|
||||||
for (int i = 0; i < 48 && i2cBus->available(); ++i) {
|
|
||||||
response[i] = i2cBus->read();
|
|
||||||
}
|
|
||||||
|
|
||||||
char productName[33] = {0};
|
|
||||||
int j = 0;
|
|
||||||
for (int i = 0; i < 48 && j < 32; i += 3) {
|
|
||||||
if (response[i] >= 32 && response[i] <= 126)
|
|
||||||
productName[j++] = response[i];
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (response[i + 1] >= 32 && response[i + 1] <= 126)
|
|
||||||
productName[j++] = response[i + 1];
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(productName);
|
|
||||||
}
|
|
||||||
|
|
||||||
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
||||||
case ADDR: \
|
case ADDR: \
|
||||||
logFoundDevice(__VA_ARGS__); \
|
logFoundDevice(__VA_ARGS__); \
|
||||||
@@ -219,8 +175,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
type = NONE;
|
type = NONE;
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
switch (addr.address) {
|
switch (addr.address) {
|
||||||
case SSD1306_ADDRESS_H:
|
case SSD1306_ADDRESS:
|
||||||
case SSD1306_ADDRESS_L:
|
|
||||||
type = probeOLED(addr);
|
type = probeOLED(addr);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -247,10 +202,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address)
|
SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef PCF85063_RTC
|
|
||||||
SCAN_SIMPLE_CASE(PCF85063_RTC, RTC_PCF85063, "PCF85063", (uint8_t)addr.address)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
case CARDKB_ADDR:
|
case CARDKB_ADDR:
|
||||||
// Do we have the RAK14006 instead?
|
// Do we have the RAK14006 instead?
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1);
|
||||||
@@ -427,11 +378,11 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
}
|
}
|
||||||
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
|
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
|
||||||
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
|
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
|
||||||
if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2);
|
||||||
|
if (registerValue == 0x5449) {
|
||||||
type = OPT3001;
|
type = OPT3001;
|
||||||
logFoundDevice("OPT3001", (uint8_t)addr.address);
|
logFoundDevice("OPT3001", (uint8_t)addr.address);
|
||||||
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) !=
|
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number
|
||||||
0) { // unique SHT4x serial number (6 bytes inc. CRC)
|
|
||||||
type = SHT4X;
|
type = SHT4X;
|
||||||
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
||||||
} else {
|
} else {
|
||||||
@@ -457,7 +408,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
|
|
||||||
case LPS22HB_ADDR_ALT:
|
case LPS22HB_ADDR_ALT:
|
||||||
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
|
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
|
||||||
SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
|
SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310", (uint8_t)addr.address)
|
||||||
|
|
||||||
case QMI8658_ADDR:
|
case QMI8658_ADDR:
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID
|
||||||
@@ -487,7 +438,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
#ifdef HAS_QMA6100P
|
#ifdef HAS_QMA6100P
|
||||||
SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address)
|
SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address)
|
||||||
#else
|
#else
|
||||||
SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address)
|
SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address)
|
||||||
#endif
|
#endif
|
||||||
case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT
|
case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2);
|
||||||
@@ -532,7 +483,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TSL25911_ADDR:
|
case TSL25911_ADDR:
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xA0 | 0x12), 1);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1);
|
||||||
if (registerValue == 0x50) {
|
if (registerValue == 0x50) {
|
||||||
type = TSL2591;
|
type = TSL2591;
|
||||||
logFoundDevice("TSL25911", (uint8_t)addr.address);
|
logFoundDevice("TSL25911", (uint8_t)addr.address);
|
||||||
@@ -608,9 +559,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
|
case ICM20948_ADDR: // same as BMX160_ADDR
|
||||||
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
|
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
|
||||||
// ICM20948 Register check
|
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
|
||||||
#ifdef HAS_ICM20948
|
#ifdef HAS_ICM20948
|
||||||
type = ICM20948;
|
type = ICM20948;
|
||||||
@@ -621,31 +571,14 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
type = ICM20948;
|
type = ICM20948;
|
||||||
logFoundDevice("ICM20948", (uint8_t)addr.address);
|
logFoundDevice("ICM20948", (uint8_t)addr.address);
|
||||||
break;
|
break;
|
||||||
|
} else if (addr.address == BMX160_ADDR) {
|
||||||
|
type = BMX160;
|
||||||
|
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||||
|
break;
|
||||||
} else {
|
} else {
|
||||||
String prod = "";
|
type = MPU6050;
|
||||||
prod = readSEN5xProductName(i2cBus, addr.address);
|
logFoundDevice("MPU6050", (uint8_t)addr.address);
|
||||||
if (prod.startsWith("SEN55")) {
|
break;
|
||||||
type = SEN5X;
|
|
||||||
logFoundDevice("Sensirion SEN55", addr.address);
|
|
||||||
break;
|
|
||||||
} else if (prod.startsWith("SEN54")) {
|
|
||||||
type = SEN5X;
|
|
||||||
logFoundDevice("Sensirion SEN54", addr.address);
|
|
||||||
break;
|
|
||||||
} else if (prod.startsWith("SEN50")) {
|
|
||||||
type = SEN5X;
|
|
||||||
logFoundDevice("Sensirion SEN50", addr.address);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (addr.address == BMX160_ADDR) {
|
|
||||||
type = BMX160;
|
|
||||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
type = MPU6050;
|
|
||||||
logFoundDevice("MPU6050", (uint8_t)addr.address);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
#include "reClockI2C.h"
|
|
||||||
#include "ScanI2CTwoWire.h"
|
|
||||||
|
|
||||||
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force)
|
|
||||||
{
|
|
||||||
|
|
||||||
uint32_t currentClock = 0;
|
|
||||||
|
|
||||||
/* See https://github.com/arduino/Arduino/issues/11457
|
|
||||||
Currently, only ESP32 can getClock()
|
|
||||||
While all cores can setClock()
|
|
||||||
https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
|
|
||||||
https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
|
|
||||||
https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
|
|
||||||
For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
|
|
||||||
we need to reclock I2C and set it back to the previous desired speed.
|
|
||||||
Only for cases where we can know OR predefine the speed, we can do this.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO add getClock function or return a predefined clock speed per variant?
|
|
||||||
#ifdef CAN_RECLOCK_I2C
|
|
||||||
currentClock = i2cBus->getClock();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if ((currentClock != desiredClock) || force) {
|
|
||||||
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
|
|
||||||
i2cBus->setClock(desiredClock);
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentClock;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
#ifndef RECLOCK_I2C_
|
|
||||||
#define RECLOCK_I2C_
|
|
||||||
|
|
||||||
#include "ScanI2CTwoWire.h"
|
|
||||||
#include <Wire.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
+9
-15
@@ -896,21 +896,18 @@ void GPS::writePinEN(bool on)
|
|||||||
void GPS::writePinStandby(bool standby)
|
void GPS::writePinStandby(bool standby)
|
||||||
{
|
{
|
||||||
#ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones
|
#ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones
|
||||||
bool val;
|
|
||||||
if (standby)
|
// Determine the new value for the pin
|
||||||
val = GPS_STANDBY_ACTIVE;
|
// Normally: active HIGH for awake
|
||||||
else
|
#ifdef PIN_GPS_STANDBY_INVERTED
|
||||||
val = !GPS_STANDBY_ACTIVE;
|
bool val = standby;
|
||||||
|
#else
|
||||||
|
bool val = !standby;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Write and log
|
// Write and log
|
||||||
pinMode(PIN_GPS_STANDBY, OUTPUT);
|
pinMode(PIN_GPS_STANDBY, OUTPUT);
|
||||||
digitalWrite(PIN_GPS_STANDBY, val);
|
digitalWrite(PIN_GPS_STANDBY, val);
|
||||||
|
|
||||||
// Enter backup mode on PA1010D; TODO: may be applicable to other MTK GPS too
|
|
||||||
if (IS_ONE_OF(gnssModel, GNSS_MODEL_MTK_PA1010D)) {
|
|
||||||
_serial_gps->write("$PMTK225,4*2F\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef GPS_DEBUG
|
#ifdef GPS_DEBUG
|
||||||
LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW");
|
LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW");
|
||||||
#endif
|
#endif
|
||||||
@@ -937,11 +934,8 @@ void GPS::setPowerPMU(bool on)
|
|||||||
// t-beam v1.2 GNSS power channel
|
// t-beam v1.2 GNSS power channel
|
||||||
on ? PMU->enablePowerOutput(XPOWERS_ALDO3) : PMU->disablePowerOutput(XPOWERS_ALDO3);
|
on ? PMU->enablePowerOutput(XPOWERS_ALDO3) : PMU->disablePowerOutput(XPOWERS_ALDO3);
|
||||||
} else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) {
|
} else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) {
|
||||||
// t-beam-s3-core GNSS power channel
|
// t-beam-s3-core GNSS power channel
|
||||||
on ? PMU->enablePowerOutput(XPOWERS_ALDO4) : PMU->disablePowerOutput(XPOWERS_ALDO4);
|
on ? PMU->enablePowerOutput(XPOWERS_ALDO4) : PMU->disablePowerOutput(XPOWERS_ALDO4);
|
||||||
} else if (HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) {
|
|
||||||
// t-watch-s3-plus GNSS power channel
|
|
||||||
on ? PMU->enablePowerOutput(XPOWERS_BLDO1) : PMU->disablePowerOutput(XPOWERS_BLDO1);
|
|
||||||
}
|
}
|
||||||
} else if (model == XPOWERS_AXP192) {
|
} else if (model == XPOWERS_AXP192) {
|
||||||
// t-beam v1.1 GNSS power channel
|
// t-beam v1.1 GNSS power channel
|
||||||
|
|||||||
@@ -16,11 +16,6 @@
|
|||||||
#define GPS_EN_ACTIVE 1
|
#define GPS_EN_ACTIVE 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Allow defining the polarity of the STANDBY output. default is LOW for standby
|
|
||||||
#ifndef GPS_STANDBY_ACTIVE
|
|
||||||
#define GPS_STANDBY_ACTIVE LOW
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL;
|
static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL;
|
||||||
static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000;
|
static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000;
|
||||||
|
|
||||||
|
|||||||
+27
-38
@@ -66,28 +66,26 @@ RTCSetResult readFromRTC()
|
|||||||
currentQuality = RTCQualityDevice;
|
currentQuality = RTCQualityDevice;
|
||||||
}
|
}
|
||||||
return RTCSetResultSuccess;
|
return RTCSetResultSuccess;
|
||||||
} else {
|
|
||||||
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
|
|
||||||
}
|
}
|
||||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
#elif defined(PCF8563_RTC)
|
||||||
#if defined(PCF8563_RTC)
|
|
||||||
if (rtc_found.address == PCF8563_RTC) {
|
if (rtc_found.address == PCF8563_RTC) {
|
||||||
SensorPCF8563 rtc;
|
|
||||||
#elif defined(PCF85063_RTC)
|
|
||||||
if (rtc_found.address == PCF85063_RTC) {
|
|
||||||
SensorPCF85063 rtc;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
|
PCF8563_Class rtc;
|
||||||
|
|
||||||
#if WIRE_INTERFACES_COUNT == 2
|
#if WIRE_INTERFACES_COUNT == 2
|
||||||
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
||||||
#else
|
#else
|
||||||
rtc.begin(Wire);
|
rtc.begin();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
RTC_DateTime datetime = rtc.getDateTime();
|
auto tc = rtc.getDateTime();
|
||||||
tm t = datetime.toUnixTime();
|
tm t;
|
||||||
|
t.tm_year = tc.year - 1900;
|
||||||
|
t.tm_mon = tc.month - 1;
|
||||||
|
t.tm_mday = tc.day;
|
||||||
|
t.tm_hour = tc.hour;
|
||||||
|
t.tm_min = tc.minute;
|
||||||
|
t.tm_sec = tc.second;
|
||||||
tv.tv_sec = gm_mktime(&t);
|
tv.tv_sec = gm_mktime(&t);
|
||||||
tv.tv_usec = 0;
|
tv.tv_usec = 0;
|
||||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||||
@@ -102,16 +100,14 @@ RTCSetResult readFromRTC()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900,
|
LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
|
||||||
t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
|
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
|
||||||
if (currentQuality == RTCQualityNone) {
|
if (currentQuality == RTCQualityNone) {
|
||||||
timeStartMsec = now;
|
timeStartMsec = now;
|
||||||
zeroOffsetSecs = tv.tv_sec;
|
zeroOffsetSecs = tv.tv_sec;
|
||||||
currentQuality = RTCQualityDevice;
|
currentQuality = RTCQualityDevice;
|
||||||
}
|
}
|
||||||
return RTCSetResultSuccess;
|
return RTCSetResultSuccess;
|
||||||
} else {
|
|
||||||
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
|
|
||||||
}
|
}
|
||||||
#elif defined(RX8130CE_RTC)
|
#elif defined(RX8130CE_RTC)
|
||||||
if (rtc_found.address == RX8130CE_RTC) {
|
if (rtc_found.address == RX8130CE_RTC) {
|
||||||
@@ -236,30 +232,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
|||||||
rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
|
rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
|
||||||
LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
|
LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
|
||||||
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||||
} else {
|
|
||||||
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
|
|
||||||
}
|
}
|
||||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
#elif defined(PCF8563_RTC)
|
||||||
#if defined(PCF8563_RTC)
|
|
||||||
if (rtc_found.address == PCF8563_RTC) {
|
if (rtc_found.address == PCF8563_RTC) {
|
||||||
SensorPCF8563 rtc;
|
PCF8563_Class rtc;
|
||||||
#elif defined(PCF85063_RTC)
|
|
||||||
if (rtc_found.address == PCF85063_RTC) {
|
|
||||||
SensorPCF85063 rtc;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if WIRE_INTERFACES_COUNT == 2
|
#if WIRE_INTERFACES_COUNT == 2
|
||||||
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
||||||
#else
|
#else
|
||||||
rtc.begin(Wire);
|
rtc.begin();
|
||||||
#endif
|
#endif
|
||||||
tm *t = gmtime(&tv->tv_sec);
|
tm *t = gmtime(&tv->tv_sec);
|
||||||
rtc.setDateTime(*t);
|
rtc.setDateTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
|
||||||
LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1,
|
LOG_DEBUG("PCF8563_RTC setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
|
||||||
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||||
} else {
|
|
||||||
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
|
|
||||||
}
|
}
|
||||||
#elif defined(RX8130CE_RTC)
|
#elif defined(RX8130CE_RTC)
|
||||||
if (rtc_found.address == RX8130CE_RTC) {
|
if (rtc_found.address == RX8130CE_RTC) {
|
||||||
@@ -280,7 +266,11 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
|||||||
settimeofday(tv, NULL);
|
settimeofday(tv, NULL);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// nrf52 doesn't have a readable RTC (yet - software not written)
|
||||||
|
#if HAS_RTC
|
||||||
readFromRTC();
|
readFromRTC();
|
||||||
|
#endif
|
||||||
|
|
||||||
return RTCSetResultSuccess;
|
return RTCSetResultSuccess;
|
||||||
} else {
|
} else {
|
||||||
return RTCSetResultNotSet; // RTC was already set with a higher quality time
|
return RTCSetResultNotSet; // RTC was already set with a higher quality time
|
||||||
@@ -397,7 +387,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local)
|
|||||||
return (currentQuality >= minQuality) ? getTime(local) : 0;
|
return (currentQuality >= minQuality) ? getTime(local) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
time_t gm_mktime(const struct tm *tm)
|
time_t gm_mktime(struct tm *tm)
|
||||||
{
|
{
|
||||||
#if !MESHTASTIC_EXCLUDE_TZ
|
#if !MESHTASTIC_EXCLUDE_TZ
|
||||||
time_t result = 0;
|
time_t result = 0;
|
||||||
@@ -413,8 +403,8 @@ time_t gm_mktime(const struct tm *tm)
|
|||||||
days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400);
|
days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400);
|
||||||
|
|
||||||
// Now, within this tm->year, compute the days *before* this tm->month starts.
|
// Now, within this tm->year, compute the days *before* this tm->month starts.
|
||||||
static const int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
|
int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
|
||||||
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
|
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
|
||||||
|
|
||||||
// If this is a leap year, and we're past February, add a day:
|
// If this is a leap year, and we're past February, add a day:
|
||||||
if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) {
|
if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) {
|
||||||
@@ -435,7 +425,6 @@ time_t gm_mktime(const struct tm *tm)
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
#else
|
#else
|
||||||
struct tm tmCopy = *tm;
|
return mktime(tm);
|
||||||
return mktime(&tmCopy);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false);
|
|||||||
|
|
||||||
RTCSetResult readFromRTC();
|
RTCSetResult readFromRTC();
|
||||||
|
|
||||||
time_t gm_mktime(const struct tm *tm);
|
time_t gm_mktime(struct tm *tm);
|
||||||
|
|
||||||
#define SEC_PER_DAY 86400
|
#define SEC_PER_DAY 86400
|
||||||
#define SEC_PER_HOUR 3600
|
#define SEC_PER_HOUR 3600
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ bool EInkDisplay::connect()
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) || defined(TTGO_T_ECHO_PLUS)
|
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE)
|
||||||
{
|
{
|
||||||
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
|
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,6 @@
|
|||||||
#include "GxEPD2Multi.h"
|
#include "GxEPD2Multi.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Limit how often we push a full E-Ink refresh. T-Deck Pro needs faster updates for typing.
|
|
||||||
#ifndef EINK_FORCE_DISPLAY_THROTTLE_MS
|
|
||||||
#if defined(T_DECK_PRO)
|
|
||||||
#define EINK_FORCE_DISPLAY_THROTTLE_MS 200
|
|
||||||
#else
|
|
||||||
#define EINK_FORCE_DISPLAY_THROTTLE_MS 1000
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation.
|
* An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation.
|
||||||
*
|
*
|
||||||
@@ -51,7 +42,7 @@ class EInkDisplay : public OLEDDisplay
|
|||||||
*
|
*
|
||||||
* @return true if we did draw the screen
|
* @return true if we did draw the screen
|
||||||
*/
|
*/
|
||||||
virtual bool forceDisplay(uint32_t msecLimit = EINK_FORCE_DISPLAY_THROTTLE_MS);
|
virtual bool forceDisplay(uint32_t msecLimit = 1000);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run any code needed to complete an update, after the physical refresh has completed.
|
* Run any code needed to complete an update, after the physical refresh has completed.
|
||||||
|
|||||||
+149
-215
@@ -46,7 +46,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#endif
|
#endif
|
||||||
#include "FSCommon.h"
|
#include "FSCommon.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "MessageStore.h"
|
|
||||||
#include "RadioLibInterface.h"
|
#include "RadioLibInterface.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
@@ -65,7 +64,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "modules/WaypointModule.h"
|
#include "modules/WaypointModule.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
extern MessageStore messageStore;
|
|
||||||
|
using graphics::Emote;
|
||||||
|
using graphics::emotes;
|
||||||
|
using graphics::numEmotes;
|
||||||
|
|
||||||
#if USE_TFTDISPLAY
|
#if USE_TFTDISPLAY
|
||||||
extern uint16_t TFT_MESH;
|
extern uint16_t TFT_MESH;
|
||||||
@@ -117,6 +119,10 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100};
|
|||||||
// we'll need to hold onto pointers for the modules that can draw a frame.
|
// we'll need to hold onto pointers for the modules that can draw a frame.
|
||||||
std::vector<MeshModule *> moduleFrames;
|
std::vector<MeshModule *> moduleFrames;
|
||||||
|
|
||||||
|
// Global variables for screen function overlay symbols
|
||||||
|
std::vector<std::string> functionSymbol;
|
||||||
|
std::string functionSymbolString;
|
||||||
|
|
||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
// GeoCoord object for the screen
|
// GeoCoord object for the screen
|
||||||
GeoCoord geoCoord;
|
GeoCoord geoCoord;
|
||||||
@@ -257,11 +263,19 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
|||||||
} else {
|
} else {
|
||||||
// otherwise, just display the module frame that's aligned with the current frame
|
// otherwise, just display the module frame that's aligned with the current frame
|
||||||
module_frame = state->currentFrame;
|
module_frame = state->currentFrame;
|
||||||
|
// LOG_DEBUG("Screen is not in transition. Frame: %d", module_frame);
|
||||||
}
|
}
|
||||||
|
// LOG_DEBUG("Draw Module Frame %d", module_frame);
|
||||||
MeshModule &pi = *moduleFrames.at(module_frame);
|
MeshModule &pi = *moduleFrames.at(module_frame);
|
||||||
pi.drawFrame(display, state, x, y);
|
pi.drawFrame(display, state, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled
|
||||||
|
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet)
|
||||||
|
{
|
||||||
|
return packet->from != 0 && !moduleConfig.store_forward.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a recent lat/lon return a guess of the heading the user is walking on.
|
* Given a recent lat/lon return a guess of the heading the user is walking on.
|
||||||
*
|
*
|
||||||
@@ -308,28 +322,17 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
|||||||
{
|
{
|
||||||
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
||||||
|
|
||||||
|
LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color);
|
||||||
int32_t rawRGB = uiconfig.screen_rgb_color;
|
int32_t rawRGB = uiconfig.screen_rgb_color;
|
||||||
|
|
||||||
// Only validate the combined value once
|
|
||||||
if (rawRGB > 0 && rawRGB <= 255255255) {
|
if (rawRGB > 0 && rawRGB <= 255255255) {
|
||||||
LOG_INFO("Setting screen RGB color to user chosen: 0x%06X", rawRGB);
|
uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF;
|
||||||
// Extract each component as a normal int first
|
uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF;
|
||||||
int r = (rawRGB >> 16) & 0xFF;
|
uint8_t TFT_MESH_b = rawRGB & 0xFF;
|
||||||
int g = (rawRGB >> 8) & 0xFF;
|
LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
|
||||||
int b = rawRGB & 0xFF;
|
|
||||||
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) {
|
||||||
TFT_MESH = COLOR565(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
|
TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
|
||||||
}
|
}
|
||||||
#ifdef TFT_MESH_OVERRIDE
|
|
||||||
} else if (rawRGB == 0) {
|
|
||||||
LOG_INFO("Setting screen RGB color to TFT_MESH_OVERRIDE: 0x%04X", TFT_MESH_OVERRIDE);
|
|
||||||
// Default to TFT_MESH_OVERRIDE if available
|
|
||||||
TFT_MESH = TFT_MESH_OVERRIDE;
|
|
||||||
#endif
|
|
||||||
} else {
|
|
||||||
// Default best readable yellow color
|
|
||||||
LOG_INFO("Setting screen RGB color to default: (255,255,128)");
|
|
||||||
TFT_MESH = COLOR565(255, 255, 128);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
|
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
|
||||||
@@ -547,10 +550,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|||||||
void Screen::setup()
|
void Screen::setup()
|
||||||
{
|
{
|
||||||
|
|
||||||
// Enable display rendering
|
// === Enable display rendering ===
|
||||||
useDisplay = true;
|
useDisplay = true;
|
||||||
|
|
||||||
// Load saved brightness from UI config
|
// === Load saved brightness from UI config ===
|
||||||
// For OLED displays (SSD1306), default brightness is 255 if not set
|
// For OLED displays (SSD1306), default brightness is 255 if not set
|
||||||
if (uiconfig.screen_brightness == 0) {
|
if (uiconfig.screen_brightness == 0) {
|
||||||
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||||
@@ -562,7 +565,7 @@ void Screen::setup()
|
|||||||
brightness = uiconfig.screen_brightness;
|
brightness = uiconfig.screen_brightness;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect OLED subtype (if supported by board variant)
|
// === Detect OLED subtype (if supported by board variant) ===
|
||||||
#ifdef AutoOLEDWire_h
|
#ifdef AutoOLEDWire_h
|
||||||
if (isAUTOOled)
|
if (isAUTOOled)
|
||||||
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
|
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
|
||||||
@@ -584,7 +587,7 @@ void Screen::setup()
|
|||||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
|
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Initialize display and UI system
|
// === Initialize display and UI system ===
|
||||||
ui->init();
|
ui->init();
|
||||||
displayWidth = dispdev->width();
|
displayWidth = dispdev->width();
|
||||||
displayHeight = dispdev->height();
|
displayHeight = dispdev->height();
|
||||||
@@ -596,7 +599,7 @@ void Screen::setup()
|
|||||||
ui->disableAllIndicators(); // Disable page indicator dots
|
ui->disableAllIndicators(); // Disable page indicator dots
|
||||||
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
|
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
|
||||||
|
|
||||||
// Apply loaded brightness
|
// === Apply loaded brightness ===
|
||||||
#if defined(ST7789_CS)
|
#if defined(ST7789_CS)
|
||||||
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||||||
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
|
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
|
||||||
@@ -604,20 +607,20 @@ void Screen::setup()
|
|||||||
#endif
|
#endif
|
||||||
LOG_INFO("Applied screen brightness: %d", brightness);
|
LOG_INFO("Applied screen brightness: %d", brightness);
|
||||||
|
|
||||||
// Set custom overlay callbacks
|
// === Set custom overlay callbacks ===
|
||||||
static OverlayCallback overlays[] = {
|
static OverlayCallback overlays[] = {
|
||||||
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
||||||
};
|
};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
|
||||||
// Enable UTF-8 to display mapping
|
// === Enable UTF-8 to display mapping ===
|
||||||
dispdev->setFontTableLookupFunction(customFontTableLookup);
|
dispdev->setFontTableLookupFunction(customFontTableLookup);
|
||||||
|
|
||||||
#ifdef USERPREFS_OEM_TEXT
|
#ifdef USERPREFS_OEM_TEXT
|
||||||
logo_timeout *= 2; // Give more time for branded boot logos
|
logo_timeout *= 2; // Give more time for branded boot logos
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Configure alert frames (e.g., "Resuming..." or region name)
|
// === Configure alert frames (e.g., "Resuming..." or region name) ===
|
||||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh
|
||||||
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
|
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
@@ -633,10 +636,10 @@ void Screen::setup()
|
|||||||
ui->setFrames(alertFrames, 1);
|
ui->setFrames(alertFrames, 1);
|
||||||
ui->disableAutoTransition(); // Require manual navigation between frames
|
ui->disableAutoTransition(); // Require manual navigation between frames
|
||||||
|
|
||||||
// Log buffer for on-screen logs (3 lines max)
|
// === Log buffer for on-screen logs (3 lines max) ===
|
||||||
dispdev->setLogBuffer(3, 32);
|
dispdev->setLogBuffer(3, 32);
|
||||||
|
|
||||||
// Optional screen mirroring or flipping (e.g. for T-Beam orientation)
|
// === Optional screen mirroring or flipping (e.g. for T-Beam orientation) ===
|
||||||
#ifdef SCREEN_MIRROR
|
#ifdef SCREEN_MIRROR
|
||||||
dispdev->mirrorScreen();
|
dispdev->mirrorScreen();
|
||||||
#else
|
#else
|
||||||
@@ -654,7 +657,7 @@ void Screen::setup()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Generate device ID from MAC address
|
// === Generate device ID from MAC address ===
|
||||||
uint8_t dmac[6];
|
uint8_t dmac[6];
|
||||||
getMacAddr(dmac);
|
getMacAddr(dmac);
|
||||||
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
||||||
@@ -663,9 +666,9 @@ void Screen::setup()
|
|||||||
handleSetOn(false); // Ensure proper init for Arduino targets
|
handleSetOn(false); // Ensure proper init for Arduino targets
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Turn on display and trigger first draw
|
// === Turn on display and trigger first draw ===
|
||||||
handleSetOn(true);
|
handleSetOn(true);
|
||||||
graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width());
|
determineResolution(dispdev->height(), dispdev->width());
|
||||||
ui->update();
|
ui->update();
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
|
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
|
||||||
@@ -686,7 +689,7 @@ void Screen::setup()
|
|||||||
touchScreenImpl1->init();
|
touchScreenImpl1->init();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Subscribe to device status updates
|
// === Subscribe to device status updates ===
|
||||||
powerStatusObserver.observe(&powerStatus->onNewStatus);
|
powerStatusObserver.observe(&powerStatus->onNewStatus);
|
||||||
gpsStatusObserver.observe(&gpsStatus->onNewStatus);
|
gpsStatusObserver.observe(&gpsStatus->onNewStatus);
|
||||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||||
@@ -694,14 +697,12 @@ void Screen::setup()
|
|||||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||||
adminMessageObserver.observe(adminModule);
|
adminMessageObserver.observe(adminModule);
|
||||||
#endif
|
#endif
|
||||||
|
if (textMessageModule)
|
||||||
|
textMessageObserver.observe(textMessageModule);
|
||||||
if (inputBroker)
|
if (inputBroker)
|
||||||
inputObserver.observe(inputBroker);
|
inputObserver.observe(inputBroker);
|
||||||
|
|
||||||
// Load persisted messages into RAM
|
// === Notify modules that support UI events ===
|
||||||
messageStore.loadFromFlash();
|
|
||||||
LOG_INFO("MessageStore loaded from flash");
|
|
||||||
|
|
||||||
// Notify modules that support UI events
|
|
||||||
MeshModule::observeUIEvents(&uiFrameEventObserver);
|
MeshModule::observeUIEvents(&uiFrameEventObserver);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,23 +773,6 @@ int32_t Screen::runOnce()
|
|||||||
if (displayHeight == 0) {
|
if (displayHeight == 0) {
|
||||||
displayHeight = dispdev->getHeight();
|
displayHeight = dispdev->getHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect frame transitions and clear message cache when leaving text message screen
|
|
||||||
{
|
|
||||||
static int8_t lastFrameIndex = -1;
|
|
||||||
int8_t currentFrameIndex = ui->getUiState()->currentFrame;
|
|
||||||
int8_t textMsgIndex = framesetInfo.positions.textMessage;
|
|
||||||
|
|
||||||
if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) {
|
|
||||||
|
|
||||||
if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) {
|
|
||||||
graphics::MessageRenderer::clearMessageCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastFrameIndex = currentFrameIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
menuHandler::handleMenuSwitch(dispdev);
|
menuHandler::handleMenuSwitch(dispdev);
|
||||||
|
|
||||||
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
||||||
@@ -825,7 +809,7 @@ int32_t Screen::runOnce()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0 && !suppressRebootBanner) {
|
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
|
||||||
showSimpleBanner("Rebooting...", 0);
|
showSimpleBanner("Rebooting...", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,17 +828,17 @@ int32_t Screen::runOnce()
|
|||||||
break;
|
break;
|
||||||
case Cmd::ON_PRESS:
|
case Cmd::ON_PRESS:
|
||||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
showFrame(FrameDirection::NEXT);
|
handleOnPress();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Cmd::SHOW_PREV_FRAME:
|
case Cmd::SHOW_PREV_FRAME:
|
||||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
showFrame(FrameDirection::PREVIOUS);
|
handleShowPrevFrame();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Cmd::SHOW_NEXT_FRAME:
|
case Cmd::SHOW_NEXT_FRAME:
|
||||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
showFrame(FrameDirection::NEXT);
|
handleShowNextFrame();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Cmd::START_ALERT_FRAME: {
|
case Cmd::START_ALERT_FRAME: {
|
||||||
@@ -875,7 +859,6 @@ int32_t Screen::runOnce()
|
|||||||
break;
|
break;
|
||||||
case Cmd::STOP_ALERT_FRAME:
|
case Cmd::STOP_ALERT_FRAME:
|
||||||
NotificationRenderer::pauseBanner = false;
|
NotificationRenderer::pauseBanner = false;
|
||||||
break;
|
|
||||||
case Cmd::STOP_BOOT_SCREEN:
|
case Cmd::STOP_BOOT_SCREEN:
|
||||||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
||||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
@@ -1046,6 +1029,9 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Declare this early so it’s available in FOCUS_PRESERVE block
|
||||||
|
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
|
||||||
|
|
||||||
if (!hiddenFrames.home) {
|
if (!hiddenFrames.home) {
|
||||||
fsi.positions.home = numframes;
|
fsi.positions.home = numframes;
|
||||||
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
|
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
|
||||||
@@ -1057,16 +1043,11 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
indicatorIcons.push_back(icon_mail);
|
indicatorIcons.push_back(icon_mail);
|
||||||
|
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
if (!hiddenFrames.nodelist_nodes) {
|
if (!hiddenFrames.nodelist) {
|
||||||
fsi.positions.nodelist_nodes = numframes;
|
fsi.positions.nodelist = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
|
||||||
indicatorIcons.push_back(icon_nodes);
|
indicatorIcons.push_back(icon_nodes);
|
||||||
}
|
}
|
||||||
if (!hiddenFrames.nodelist_location) {
|
|
||||||
fsi.positions.nodelist_location = numframes;
|
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location;
|
|
||||||
indicatorIcons.push_back(icon_list);
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Show detailed node views only on E-Ink builds
|
// Show detailed node views only on E-Ink builds
|
||||||
@@ -1088,13 +1069,11 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
#ifdef USE_EINK
|
|
||||||
if (!hiddenFrames.nodelist_bearings) {
|
if (!hiddenFrames.nodelist_bearings) {
|
||||||
fsi.positions.nodelist_bearings = numframes;
|
fsi.positions.nodelist_bearings = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
||||||
indicatorIcons.push_back(icon_list);
|
indicatorIcons.push_back(icon_list);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
if (!hiddenFrames.gps) {
|
if (!hiddenFrames.gps) {
|
||||||
fsi.positions.gps = numframes;
|
fsi.positions.gps = numframes;
|
||||||
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
|
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
|
||||||
@@ -1194,7 +1173,7 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
||||||
this->frameCount = numframes; // Save frame count for use in custom overlay
|
this->frameCount = numframes; // ✅ Save frame count for use in custom overlay
|
||||||
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
|
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
|
||||||
|
|
||||||
ui->setFrames(normalFrames, numframes);
|
ui->setFrames(normalFrames, numframes);
|
||||||
@@ -1214,6 +1193,10 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
case FOCUS_FAULT:
|
case FOCUS_FAULT:
|
||||||
ui->switchToFrame(fsi.positions.fault);
|
ui->switchToFrame(fsi.positions.fault);
|
||||||
break;
|
break;
|
||||||
|
case FOCUS_TEXTMESSAGE:
|
||||||
|
hasUnreadMessage = false; // ✅ Clear when message is *viewed*
|
||||||
|
ui->switchToFrame(fsi.positions.textMessage);
|
||||||
|
break;
|
||||||
case FOCUS_MODULE:
|
case FOCUS_MODULE:
|
||||||
// Whichever frame was marked by MeshModule::requestFocus(), if any
|
// Whichever frame was marked by MeshModule::requestFocus(), if any
|
||||||
// If no module requested focus, will show the first frame instead
|
// If no module requested focus, will show the first frame instead
|
||||||
@@ -1256,11 +1239,8 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
|
|||||||
void Screen::toggleFrameVisibility(const std::string &frameName)
|
void Screen::toggleFrameVisibility(const std::string &frameName)
|
||||||
{
|
{
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
if (frameName == "nodelist_nodes") {
|
if (frameName == "nodelist") {
|
||||||
hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes;
|
hiddenFrames.nodelist = !hiddenFrames.nodelist;
|
||||||
}
|
|
||||||
if (frameName == "nodelist_location") {
|
|
||||||
hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location;
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
@@ -1275,11 +1255,9 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
#ifdef USE_EINK
|
|
||||||
if (frameName == "nodelist_bearings") {
|
if (frameName == "nodelist_bearings") {
|
||||||
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
|
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
if (frameName == "gps") {
|
if (frameName == "gps") {
|
||||||
hiddenFrames.gps = !hiddenFrames.gps;
|
hiddenFrames.gps = !hiddenFrames.gps;
|
||||||
}
|
}
|
||||||
@@ -1301,10 +1279,8 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
|
|||||||
bool Screen::isFrameHidden(const std::string &frameName) const
|
bool Screen::isFrameHidden(const std::string &frameName) const
|
||||||
{
|
{
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
if (frameName == "nodelist_nodes")
|
if (frameName == "nodelist")
|
||||||
return hiddenFrames.nodelist_nodes;
|
return hiddenFrames.nodelist;
|
||||||
if (frameName == "nodelist_location")
|
|
||||||
return hiddenFrames.nodelist_location;
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
if (frameName == "nodelist_lastheard")
|
if (frameName == "nodelist_lastheard")
|
||||||
@@ -1315,10 +1291,8 @@ bool Screen::isFrameHidden(const std::string &frameName) const
|
|||||||
return hiddenFrames.nodelist_distance;
|
return hiddenFrames.nodelist_distance;
|
||||||
#endif
|
#endif
|
||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
#ifdef USE_EINK
|
|
||||||
if (frameName == "nodelist_bearings")
|
if (frameName == "nodelist_bearings")
|
||||||
return hiddenFrames.nodelist_bearings;
|
return hiddenFrames.nodelist_bearings;
|
||||||
#endif
|
|
||||||
if (frameName == "gps")
|
if (frameName == "gps")
|
||||||
return hiddenFrames.gps;
|
return hiddenFrames.gps;
|
||||||
#endif
|
#endif
|
||||||
@@ -1334,6 +1308,37 @@ bool Screen::isFrameHidden(const std::string &frameName) const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dismisses the currently displayed screen frame, if possible
|
||||||
|
// Relevant for text message, waypoint, others in future?
|
||||||
|
// Triggered with a CardKB keycombo
|
||||||
|
void Screen::hideCurrentFrame()
|
||||||
|
{
|
||||||
|
uint8_t currentFrame = ui->getUiState()->currentFrame;
|
||||||
|
bool dismissed = false;
|
||||||
|
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
|
||||||
|
LOG_INFO("Hide Text Message");
|
||||||
|
devicestate.has_rx_text_message = false;
|
||||||
|
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
||||||
|
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
|
||||||
|
LOG_DEBUG("Hide Waypoint");
|
||||||
|
devicestate.has_rx_waypoint = false;
|
||||||
|
hiddenFrames.waypoint = true;
|
||||||
|
dismissed = true;
|
||||||
|
} else if (currentFrame == framesetInfo.positions.wifi) {
|
||||||
|
LOG_DEBUG("Hide WiFi Screen");
|
||||||
|
hiddenFrames.wifi = true;
|
||||||
|
dismissed = true;
|
||||||
|
} else if (currentFrame == framesetInfo.positions.lora) {
|
||||||
|
LOG_INFO("Hide LoRa");
|
||||||
|
hiddenFrames.lora = true;
|
||||||
|
dismissed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dismissed) {
|
||||||
|
setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Screen::handleStartFirmwareUpdateScreen()
|
void Screen::handleStartFirmwareUpdateScreen()
|
||||||
{
|
{
|
||||||
LOG_DEBUG("Show firmware screen");
|
LOG_DEBUG("Show firmware screen");
|
||||||
@@ -1386,6 +1391,28 @@ void Screen::decreaseBrightness()
|
|||||||
/* TO DO: add little popup in center of screen saying what brightness level it is set to*/
|
/* TO DO: add little popup in center of screen saying what brightness level it is set to*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Screen::setFunctionSymbol(std::string sym)
|
||||||
|
{
|
||||||
|
if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) {
|
||||||
|
functionSymbol.push_back(sym);
|
||||||
|
functionSymbolString = "";
|
||||||
|
for (auto symbol : functionSymbol) {
|
||||||
|
functionSymbolString = symbol + " " + functionSymbolString;
|
||||||
|
}
|
||||||
|
setFastFramerate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Screen::removeFunctionSymbol(std::string sym)
|
||||||
|
{
|
||||||
|
functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end());
|
||||||
|
functionSymbolString = "";
|
||||||
|
for (auto symbol : functionSymbol) {
|
||||||
|
functionSymbolString = symbol + " " + functionSymbolString;
|
||||||
|
}
|
||||||
|
setFastFramerate();
|
||||||
|
}
|
||||||
|
|
||||||
void Screen::handleOnPress()
|
void Screen::handleOnPress()
|
||||||
{
|
{
|
||||||
// If screen was off, just wake it, otherwise advance to next frame
|
// If screen was off, just wake it, otherwise advance to next frame
|
||||||
@@ -1397,17 +1424,23 @@ void Screen::handleOnPress()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::showFrame(FrameDirection direction)
|
void Screen::handleShowPrevFrame()
|
||||||
{
|
{
|
||||||
// Only advance frames when UI is stable
|
// If screen was off, just wake it, otherwise go back to previous frame
|
||||||
|
// If we are in a transition, the press must have bounced, drop it.
|
||||||
if (ui->getUiState()->frameState == FIXED) {
|
if (ui->getUiState()->frameState == FIXED) {
|
||||||
|
ui->previousFrame();
|
||||||
|
lastScreenTransition = millis();
|
||||||
|
setFastFramerate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (direction == FrameDirection::NEXT) {
|
void Screen::handleShowNextFrame()
|
||||||
ui->nextFrame();
|
{
|
||||||
} else {
|
// If screen was off, just wake it, otherwise advance to next frame
|
||||||
ui->previousFrame();
|
// If we are in a transition, the press must have bounced, drop it.
|
||||||
}
|
if (ui->getUiState()->frameState == FIXED) {
|
||||||
|
ui->nextFrame();
|
||||||
lastScreenTransition = millis();
|
lastScreenTransition = millis();
|
||||||
setFastFramerate();
|
setFastFramerate();
|
||||||
}
|
}
|
||||||
@@ -1433,6 +1466,7 @@ void Screen::setFastFramerate()
|
|||||||
|
|
||||||
int Screen::handleStatusUpdate(const meshtastic::Status *arg)
|
int Screen::handleStatusUpdate(const meshtastic::Status *arg)
|
||||||
{
|
{
|
||||||
|
// LOG_DEBUG("Screen got status update %d", arg->getStatusType());
|
||||||
switch (arg->getStatusType()) {
|
switch (arg->getStatusType()) {
|
||||||
case STATUS_TYPE_NODE:
|
case STATUS_TYPE_NODE:
|
||||||
if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) {
|
if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) {
|
||||||
@@ -1440,15 +1474,10 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg)
|
|||||||
}
|
}
|
||||||
nodeDB->updateGUI = false;
|
nodeDB->updateGUI = false;
|
||||||
break;
|
break;
|
||||||
case STATUS_TYPE_POWER: {
|
case STATUS_TYPE_POWER:
|
||||||
bool currentUSB = powerStatus->getHasUSB();
|
forceDisplay(true);
|
||||||
if (currentUSB != lastPowerUSBState) {
|
|
||||||
lastPowerUSBState = currentUSB;
|
|
||||||
forceDisplay(true);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1555,11 +1584,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
|||||||
screen->showSimpleBanner(banner, 3000);
|
screen->showSimpleBanner(banner, 3000);
|
||||||
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
|
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
|
||||||
if (longName && longName[0]) {
|
if (longName && longName[0]) {
|
||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
#if defined(M5STACK_UNITC6L)
|
||||||
strcpy(banner, "New Message");
|
strcpy(banner, "New Message");
|
||||||
} else {
|
#else
|
||||||
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
|
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
|
||||||
}
|
#endif
|
||||||
} else {
|
} else {
|
||||||
strcpy(banner, "New Message");
|
strcpy(banner, "New Message");
|
||||||
}
|
}
|
||||||
@@ -1595,26 +1624,16 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
|||||||
|
|
||||||
if (showingNormalScreen) {
|
if (showingNormalScreen) {
|
||||||
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
||||||
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) {
|
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
|
||||||
setFrames(FOCUS_MODULE);
|
setFrames(FOCUS_MODULE);
|
||||||
}
|
|
||||||
|
|
||||||
// Regenerate the frameset, while attempting to maintain focus on the current frame
|
// Regenerate the frameset, while Attempt to maintain focus on the current frame
|
||||||
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) {
|
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND)
|
||||||
setFrames(FOCUS_PRESERVE);
|
setFrames(FOCUS_PRESERVE);
|
||||||
}
|
|
||||||
|
|
||||||
// Don't regenerate the frameset, just re-draw whatever is on screen ASAP
|
// Don't regenerate the frameset, just re-draw whatever is on screen ASAP
|
||||||
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) {
|
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY)
|
||||||
setFastFramerate();
|
setFastFramerate();
|
||||||
}
|
|
||||||
|
|
||||||
// Jump directly to the Text Message screen
|
|
||||||
else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) {
|
|
||||||
setFrames(FOCUS_PRESERVE); // preserve current frame ordering
|
|
||||||
ui->switchToFrame(framesetInfo.positions.textMessage);
|
|
||||||
setFastFramerate(); // force redraw ASAP
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1652,48 +1671,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
menuHandler::handleMenuSwitch(dispdev);
|
menuHandler::handleMenuSwitch(dispdev);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
// UP/DOWN in message screen scrolls through message threads
|
|
||||||
if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
|
||||||
|
|
||||||
if (event->inputEvent == INPUT_BROKER_UP) {
|
|
||||||
if (messageStore.getMessages().empty()) {
|
|
||||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
|
||||||
} else {
|
|
||||||
graphics::MessageRenderer::scrollUp();
|
|
||||||
setFastFramerate(); // match existing behavior
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event->inputEvent == INPUT_BROKER_DOWN) {
|
|
||||||
if (messageStore.getMessages().empty()) {
|
|
||||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
|
||||||
} else {
|
|
||||||
graphics::MessageRenderer::scrollDown();
|
|
||||||
setFastFramerate();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// UP/DOWN in node list screens scrolls through node pages
|
|
||||||
if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
|
|
||||||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
|
|
||||||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
|
|
||||||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
|
||||||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
|
|
||||||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) {
|
|
||||||
if (event->inputEvent == INPUT_BROKER_UP) {
|
|
||||||
graphics::NodeListRenderer::scrollUp();
|
|
||||||
setFastFramerate();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event->inputEvent == INPUT_BROKER_DOWN) {
|
|
||||||
graphics::NodeListRenderer::scrollDown();
|
|
||||||
setFastFramerate();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Use left or right input from a keyboard to move between frames,
|
// Use left or right input from a keyboard to move between frames,
|
||||||
// so long as a mesh module isn't using these events for some other purpose
|
// so long as a mesh module isn't using these events for some other purpose
|
||||||
if (showingNormalScreen) {
|
if (showingNormalScreen) {
|
||||||
@@ -1707,59 +1685,16 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
|
|
||||||
// If no modules are using the input, move between frames
|
// If no modules are using the input, move between frames
|
||||||
if (!inputIntercepted) {
|
if (!inputIntercepted) {
|
||||||
#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2
|
|
||||||
bool handledEncoderScroll = false;
|
|
||||||
const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 &&
|
|
||||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
|
|
||||||
!messageStore.getMessages().empty());
|
|
||||||
if (isTextMessageFrame) {
|
|
||||||
if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
|
||||||
graphics::MessageRenderer::nudgeScroll(-1);
|
|
||||||
handledEncoderScroll = true;
|
|
||||||
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
|
|
||||||
graphics::MessageRenderer::nudgeScroll(1);
|
|
||||||
handledEncoderScroll = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handledEncoderScroll) {
|
|
||||||
setFastFramerate();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) {
|
if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||||
showFrame(FrameDirection::PREVIOUS);
|
showPrevFrame();
|
||||||
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
|
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||||
showFrame(FrameDirection::NEXT);
|
showNextFrame();
|
||||||
} else if (event->inputEvent == INPUT_BROKER_FN_F1) {
|
|
||||||
this->ui->switchToFrame(0);
|
|
||||||
lastScreenTransition = millis();
|
|
||||||
setFastFramerate();
|
|
||||||
} else if (event->inputEvent == INPUT_BROKER_FN_F2) {
|
|
||||||
this->ui->switchToFrame(1);
|
|
||||||
lastScreenTransition = millis();
|
|
||||||
setFastFramerate();
|
|
||||||
} else if (event->inputEvent == INPUT_BROKER_FN_F3) {
|
|
||||||
this->ui->switchToFrame(2);
|
|
||||||
lastScreenTransition = millis();
|
|
||||||
setFastFramerate();
|
|
||||||
} else if (event->inputEvent == INPUT_BROKER_FN_F4) {
|
|
||||||
this->ui->switchToFrame(3);
|
|
||||||
lastScreenTransition = millis();
|
|
||||||
setFastFramerate();
|
|
||||||
} else if (event->inputEvent == INPUT_BROKER_FN_F5) {
|
|
||||||
this->ui->switchToFrame(4);
|
|
||||||
lastScreenTransition = millis();
|
|
||||||
setFastFramerate();
|
|
||||||
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
||||||
// Long press up button for fast frame switching
|
// Long press up button for fast frame switching
|
||||||
showPrevFrame();
|
showPrevFrame();
|
||||||
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
|
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||||
// Long press down button for fast frame switching
|
// Long press down button for fast frame switching
|
||||||
showNextFrame();
|
showNextFrame();
|
||||||
} else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) &&
|
|
||||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
|
||||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
|
||||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||||
menuHandler::homeBaseMenu();
|
menuHandler::homeBaseMenu();
|
||||||
@@ -1774,21 +1709,20 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
||||||
menuHandler::loraMenu();
|
menuHandler::loraMenu();
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||||
if (!messageStore.getMessages().empty()) {
|
if (devicestate.rx_text_message.from) {
|
||||||
menuHandler::messageResponseMenu();
|
menuHandler::messageResponseMenu();
|
||||||
} else {
|
} else {
|
||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
#if defined(M5STACK_UNITC6L)
|
||||||
menuHandler::textMessageMenu();
|
menuHandler::textMessageMenu();
|
||||||
} else {
|
#else
|
||||||
menuHandler::textMessageBaseMenu();
|
menuHandler::textMessageBaseMenu();
|
||||||
}
|
#endif
|
||||||
}
|
}
|
||||||
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
||||||
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||||
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
||||||
menuHandler::favoriteBaseMenu();
|
menuHandler::favoriteBaseMenu();
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
|
||||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
|
|
||||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
|
||||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
||||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
|
||||||
@@ -1799,7 +1733,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
menuHandler::wifiBaseMenu();
|
menuHandler::wifiBaseMenu();
|
||||||
}
|
}
|
||||||
} else if (event->inputEvent == INPUT_BROKER_BACK) {
|
} else if (event->inputEvent == INPUT_BROKER_BACK) {
|
||||||
showFrame(FrameDirection::PREVIOUS);
|
showPrevFrame();
|
||||||
} else if (event->inputEvent == INPUT_BROKER_CANCEL) {
|
} else if (event->inputEvent == INPUT_BROKER_CANCEL) {
|
||||||
setOn(false);
|
setOn(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-52
@@ -40,6 +40,7 @@ class Screen
|
|||||||
FOCUS_DEFAULT, // No specific frame
|
FOCUS_DEFAULT, // No specific frame
|
||||||
FOCUS_PRESERVE, // Return to the previous frame
|
FOCUS_PRESERVE, // Return to the previous frame
|
||||||
FOCUS_FAULT,
|
FOCUS_FAULT,
|
||||||
|
FOCUS_TEXTMESSAGE,
|
||||||
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||||
FOCUS_CLOCK,
|
FOCUS_CLOCK,
|
||||||
FOCUS_SYSTEM,
|
FOCUS_SYSTEM,
|
||||||
@@ -54,6 +55,8 @@ class Screen
|
|||||||
void startFirmwareUpdateScreen() {}
|
void startFirmwareUpdateScreen() {}
|
||||||
void increaseBrightness() {}
|
void increaseBrightness() {}
|
||||||
void decreaseBrightness() {}
|
void decreaseBrightness() {}
|
||||||
|
void setFunctionSymbol(std::string) {}
|
||||||
|
void removeFunctionSymbol(std::string) {}
|
||||||
void startAlert(const char *) {}
|
void startAlert(const char *) {}
|
||||||
void showSimpleBanner(const char *message, uint32_t durationMs = 0) {}
|
void showSimpleBanner(const char *message, uint32_t durationMs = 0) {}
|
||||||
void showOverlayBanner(BannerOverlayOptions) {}
|
void showOverlayBanner(BannerOverlayOptions) {}
|
||||||
@@ -169,8 +172,6 @@ class Point
|
|||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
|
|
||||||
enum class FrameDirection { NEXT, PREVIOUS };
|
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
class Screen;
|
class Screen;
|
||||||
|
|
||||||
@@ -210,6 +211,8 @@ class Screen : public concurrency::OSThread
|
|||||||
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
|
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
|
||||||
CallbackObserver<Screen, const meshtastic::Status *> nodeStatusObserver =
|
CallbackObserver<Screen, const meshtastic::Status *> nodeStatusObserver =
|
||||||
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
|
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
|
||||||
|
CallbackObserver<Screen, const meshtastic_MeshPacket *> textMessageObserver =
|
||||||
|
CallbackObserver<Screen, const meshtastic_MeshPacket *>(this, &Screen::handleTextMessage);
|
||||||
CallbackObserver<Screen, const UIFrameEvent *> uiFrameEventObserver =
|
CallbackObserver<Screen, const UIFrameEvent *> uiFrameEventObserver =
|
||||||
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
|
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
|
||||||
CallbackObserver<Screen, const InputEvent *> inputObserver =
|
CallbackObserver<Screen, const InputEvent *> inputObserver =
|
||||||
@@ -220,10 +223,6 @@ class Screen : public concurrency::OSThread
|
|||||||
public:
|
public:
|
||||||
OLEDDisplay *getDisplayDevice() { return dispdev; }
|
OLEDDisplay *getDisplayDevice() { return dispdev; }
|
||||||
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
||||||
|
|
||||||
// Screen dimension accessors
|
|
||||||
inline int getHeight() const { return displayHeight; }
|
|
||||||
inline int getWidth() const { return displayWidth; }
|
|
||||||
size_t frameCount = 0; // Total number of active frames
|
size_t frameCount = 0; // Total number of active frames
|
||||||
~Screen();
|
~Screen();
|
||||||
|
|
||||||
@@ -232,6 +231,7 @@ class Screen : public concurrency::OSThread
|
|||||||
FOCUS_DEFAULT, // No specific frame
|
FOCUS_DEFAULT, // No specific frame
|
||||||
FOCUS_PRESERVE, // Return to the previous frame
|
FOCUS_PRESERVE, // Return to the previous frame
|
||||||
FOCUS_FAULT,
|
FOCUS_FAULT,
|
||||||
|
FOCUS_TEXTMESSAGE,
|
||||||
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||||
FOCUS_CLOCK,
|
FOCUS_CLOCK,
|
||||||
FOCUS_SYSTEM,
|
FOCUS_SYSTEM,
|
||||||
@@ -279,7 +279,6 @@ class Screen : public concurrency::OSThread
|
|||||||
void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); }
|
void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); }
|
||||||
void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); }
|
void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); }
|
||||||
void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); }
|
void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); }
|
||||||
void showFrame(FrameDirection direction);
|
|
||||||
|
|
||||||
// generic alert start
|
// generic alert start
|
||||||
void startAlert(FrameCallback _alertFrame)
|
void startAlert(FrameCallback _alertFrame)
|
||||||
@@ -347,6 +346,9 @@ class Screen : public concurrency::OSThread
|
|||||||
void increaseBrightness();
|
void increaseBrightness();
|
||||||
void decreaseBrightness();
|
void decreaseBrightness();
|
||||||
|
|
||||||
|
void setFunctionSymbol(std::string sym);
|
||||||
|
void removeFunctionSymbol(std::string sym);
|
||||||
|
|
||||||
/// Stops showing the boot screen.
|
/// Stops showing the boot screen.
|
||||||
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
|
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
|
||||||
|
|
||||||
@@ -558,42 +560,6 @@ class Screen : public concurrency::OSThread
|
|||||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5)
|
if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5)
|
||||||
return (uint8_t)0;
|
return (uint8_t)0;
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(OLED_GR)
|
|
||||||
|
|
||||||
switch (last) {
|
|
||||||
case 0xC3: {
|
|
||||||
SKIPREST = false;
|
|
||||||
return (uint8_t)(ch | 0xC0);
|
|
||||||
}
|
|
||||||
// Map UTF-8 Greek chars to Windows-1253 (CP-1253) ASCII codes
|
|
||||||
case 0xCE: {
|
|
||||||
SKIPREST = false;
|
|
||||||
// Uppercase Greek: Α-Ρ (U+0391-U+03A1) -> CP-1253 193-209
|
|
||||||
if (ch >= 145 && ch <= 161)
|
|
||||||
return (uint8_t)(ch + 48);
|
|
||||||
// Uppercase Greek: Σ-Ω (U+03A3-U+03A9) -> CP-1253 211-217
|
|
||||||
else if (ch >= 163 && ch <= 169)
|
|
||||||
return (uint8_t)(ch + 48);
|
|
||||||
// Lowercase Greek: α-ρ (U+03B1-U+03C1) -> CP-1253 225-241
|
|
||||||
else if (ch >= 177 && ch <= 193)
|
|
||||||
return (uint8_t)(ch + 48);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 0xCF: {
|
|
||||||
SKIPREST = false;
|
|
||||||
// Lowercase Greek: ς-ω (U+03C2-U+03C9) -> CP-1253 242-249
|
|
||||||
if (ch >= 130 && ch <= 137)
|
|
||||||
return (uint8_t)(ch + 112);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to strip out prefix chars for two-byte Greek char formats
|
|
||||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0xCE || ch == 0xCF)
|
|
||||||
return (uint8_t)0;
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// If we already returned an unconvertable-character symbol for this unconvertable-character sequence, return NULs for the
|
// If we already returned an unconvertable-character symbol for this unconvertable-character sequence, return NULs for the
|
||||||
@@ -613,7 +579,7 @@ class Screen : public concurrency::OSThread
|
|||||||
|
|
||||||
// Handle observer events
|
// Handle observer events
|
||||||
int handleStatusUpdate(const meshtastic::Status *arg);
|
int handleStatusUpdate(const meshtastic::Status *arg);
|
||||||
int handleTextMessage(const meshtastic_MeshPacket *packet);
|
int handleTextMessage(const meshtastic_MeshPacket *arg);
|
||||||
int handleUIFrameEvent(const UIFrameEvent *arg);
|
int handleUIFrameEvent(const UIFrameEvent *arg);
|
||||||
int handleInputEvent(const InputEvent *arg);
|
int handleInputEvent(const InputEvent *arg);
|
||||||
int handleAdminMessage(AdminModule_ObserverData *arg);
|
int handleAdminMessage(AdminModule_ObserverData *arg);
|
||||||
@@ -624,6 +590,9 @@ class Screen : public concurrency::OSThread
|
|||||||
/// Draws our SSL cert screen during boot (called from WebServer)
|
/// Draws our SSL cert screen during boot (called from WebServer)
|
||||||
void setSSLFrames();
|
void setSSLFrames();
|
||||||
|
|
||||||
|
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
|
||||||
|
void hideCurrentFrame();
|
||||||
|
|
||||||
// Menu-driven Show / Hide Toggle
|
// Menu-driven Show / Hide Toggle
|
||||||
void toggleFrameVisibility(const std::string &frameName);
|
void toggleFrameVisibility(const std::string &frameName);
|
||||||
bool isFrameHidden(const std::string &frameName) const;
|
bool isFrameHidden(const std::string &frameName) const;
|
||||||
@@ -671,6 +640,8 @@ class Screen : public concurrency::OSThread
|
|||||||
// Implementations of various commands, called from doTask().
|
// Implementations of various commands, called from doTask().
|
||||||
void handleSetOn(bool on, FrameCallback einkScreensaver = NULL);
|
void handleSetOn(bool on, FrameCallback einkScreensaver = NULL);
|
||||||
void handleOnPress();
|
void handleOnPress();
|
||||||
|
void handleShowNextFrame();
|
||||||
|
void handleShowPrevFrame();
|
||||||
void handleStartFirmwareUpdateScreen();
|
void handleStartFirmwareUpdateScreen();
|
||||||
|
|
||||||
// Info collected by setFrames method.
|
// Info collected by setFrames method.
|
||||||
@@ -690,8 +661,7 @@ class Screen : public concurrency::OSThread
|
|||||||
uint8_t gps = 255;
|
uint8_t gps = 255;
|
||||||
uint8_t home = 255;
|
uint8_t home = 255;
|
||||||
uint8_t textMessage = 255;
|
uint8_t textMessage = 255;
|
||||||
uint8_t nodelist_nodes = 255;
|
uint8_t nodelist = 255;
|
||||||
uint8_t nodelist_location = 255;
|
|
||||||
uint8_t nodelist_lastheard = 255;
|
uint8_t nodelist_lastheard = 255;
|
||||||
uint8_t nodelist_hopsignal = 255;
|
uint8_t nodelist_hopsignal = 255;
|
||||||
uint8_t nodelist_distance = 255;
|
uint8_t nodelist_distance = 255;
|
||||||
@@ -714,8 +684,7 @@ class Screen : public concurrency::OSThread
|
|||||||
bool home = false;
|
bool home = false;
|
||||||
bool clock = false;
|
bool clock = false;
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
bool nodelist_nodes = false;
|
bool nodelist = false;
|
||||||
bool nodelist_location = false;
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
bool nodelist_lastheard = false;
|
bool nodelist_lastheard = false;
|
||||||
@@ -723,9 +692,7 @@ class Screen : public concurrency::OSThread
|
|||||||
bool nodelist_distance = false;
|
bool nodelist_distance = false;
|
||||||
#endif
|
#endif
|
||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
#ifdef USE_EINK
|
|
||||||
bool nodelist_bearings = false;
|
bool nodelist_bearings = false;
|
||||||
#endif
|
|
||||||
bool gps = false;
|
bool gps = false;
|
||||||
#endif
|
#endif
|
||||||
bool lora = false;
|
bool lora = false;
|
||||||
@@ -751,8 +718,6 @@ class Screen : public concurrency::OSThread
|
|||||||
// Whether we are showing the regular screen (as opposed to booth screen or
|
// Whether we are showing the regular screen (as opposed to booth screen or
|
||||||
// Bluetooth PIN screen)
|
// Bluetooth PIN screen)
|
||||||
bool showingNormalScreen = false;
|
bool showingNormalScreen = false;
|
||||||
/// Track USB power state to only wake screen on actual power state changes
|
|
||||||
bool lastPowerUSBState = false;
|
|
||||||
|
|
||||||
// Implementation to Adjust Brightness
|
// Implementation to Adjust Brightness
|
||||||
uint8_t brightness = BRIGHTNESS_DEFAULT; // H = 254, MH = 192, ML = 130 L = 103
|
uint8_t brightness = BRIGHTNESS_DEFAULT; // H = 254, MH = 192, ML = 130 L = 103
|
||||||
|
|||||||
@@ -16,17 +16,10 @@
|
|||||||
#include "graphics/fonts/OLEDDisplayFontsCS.h"
|
#include "graphics/fonts/OLEDDisplayFontsCS.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef OLED_GR
|
|
||||||
#include "graphics/fonts/OLEDDisplayFontsGR.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
|
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
|
||||||
#include "graphics/fonts/EinkDisplayFonts.h"
|
#include "graphics/fonts/EinkDisplayFonts.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef OLED_GR
|
|
||||||
#define FONT_SMALL_LOCAL ArialMT_Plain_10_GR // Height: 13
|
|
||||||
#else
|
|
||||||
#ifdef OLED_PL
|
#ifdef OLED_PL
|
||||||
#define FONT_SMALL_LOCAL ArialMT_Plain_10_PL
|
#define FONT_SMALL_LOCAL ArialMT_Plain_10_PL
|
||||||
#else
|
#else
|
||||||
@@ -44,10 +37,6 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
#ifdef OLED_GR
|
|
||||||
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_GR // Height: 19
|
|
||||||
#else
|
|
||||||
#ifdef OLED_PL
|
#ifdef OLED_PL
|
||||||
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19
|
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19
|
||||||
#else
|
#else
|
||||||
@@ -65,10 +54,6 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
#ifdef OLED_GR
|
|
||||||
#define FONT_LARGE_LOCAL ArialMT_Plain_24_GR // Height: 28
|
|
||||||
#else
|
|
||||||
#ifdef OLED_PL
|
#ifdef OLED_PL
|
||||||
#define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28
|
#define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28
|
||||||
#else
|
#else
|
||||||
@@ -86,7 +71,6 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
|
|
||||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
#include "graphics/draw/UIRenderer.h"
|
#include "graphics/draw/UIRenderer.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "meshtastic/config.pb.h"
|
#include "meshtastic/config.pb.h"
|
||||||
#include "modules/ExternalNotificationModule.h"
|
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
#include <graphics/images.h>
|
#include <graphics/images.h>
|
||||||
@@ -16,48 +15,27 @@
|
|||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
|
|
||||||
ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth)
|
void determineResolution(int16_t screenheight, int16_t screenwidth)
|
||||||
{
|
{
|
||||||
|
|
||||||
#ifdef FORCE_LOW_RES
|
#ifdef FORCE_LOW_RES
|
||||||
return ScreenResolution::Low;
|
isHighResolution = false;
|
||||||
#else
|
return;
|
||||||
// Unit C6L and other ultra low res screens
|
|
||||||
if (screenwidth <= 64 || screenheight <= 48) {
|
|
||||||
return ScreenResolution::UltraLow;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard OLED screens
|
|
||||||
if (screenwidth > 128 && screenheight <= 64) {
|
|
||||||
return ScreenResolution::Low;
|
|
||||||
}
|
|
||||||
|
|
||||||
// High Resolutions screens like T114, TDeck, TLora Pager, etc
|
|
||||||
if (screenwidth > 128) {
|
|
||||||
return ScreenResolution::High;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to low resolution
|
|
||||||
return ScreenResolution::Low;
|
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
|
|
||||||
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second)
|
if (screenwidth > 128) {
|
||||||
{
|
isHighResolution = true;
|
||||||
hour = 0;
|
}
|
||||||
minute = 0;
|
|
||||||
second = 0;
|
if (screenwidth > 128 && screenheight <= 64) {
|
||||||
if (rtc_sec == 0)
|
isHighResolution = false;
|
||||||
return;
|
}
|
||||||
uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
|
||||||
hour = hms / SEC_PER_HOUR;
|
|
||||||
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
|
||||||
second = hms % SEC_PER_MIN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Shared External State ===
|
// === Shared External State ===
|
||||||
bool hasUnreadMessage = false;
|
bool hasUnreadMessage = false;
|
||||||
ScreenResolution currentResolution = ScreenResolution::Low;
|
bool isMuted = false;
|
||||||
|
bool isHighResolution = false;
|
||||||
|
|
||||||
// === Internal State ===
|
// === Internal State ===
|
||||||
bool isBoltVisibleShared = true;
|
bool isBoltVisibleShared = true;
|
||||||
@@ -113,7 +91,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
display->drawLine(0, 20, screenW, 20);
|
display->drawLine(0, 20, screenW, 20);
|
||||||
} else {
|
} else {
|
||||||
display->drawLine(0, 14, screenW, 14);
|
display->drawLine(0, 14, screenW, 14);
|
||||||
@@ -151,7 +129,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH);
|
bool useHorizontalBattery = (isHighResolution && screenW >= screenH);
|
||||||
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||||
|
|
||||||
int batteryX = 1;
|
int batteryX = 1;
|
||||||
@@ -161,7 +139,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
|
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
|
||||||
batteryX += 1;
|
batteryX += 1;
|
||||||
batteryY += 2;
|
batteryY += 2;
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution);
|
display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution);
|
||||||
batteryX += 20; // Icon + 1 pixel
|
batteryX += 20; // Icon + 1 pixel
|
||||||
} else {
|
} else {
|
||||||
@@ -222,8 +200,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
if (rtc_sec > 0) {
|
if (rtc_sec > 0) {
|
||||||
// === Build Time String ===
|
// === Build Time String ===
|
||||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||||
int hour, minute, second;
|
int hour = hms / SEC_PER_HOUR;
|
||||||
graphics::decomposeTime(rtc_sec, hour, minute, second);
|
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||||
|
|
||||||
// === Build Date String ===
|
// === Build Date String ===
|
||||||
@@ -231,7 +209,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false);
|
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false);
|
||||||
char dateLine[40];
|
char dateLine[40];
|
||||||
|
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr);
|
snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr);
|
||||||
} else {
|
} else {
|
||||||
if (hasUnreadMessage) {
|
if (hasUnreadMessage) {
|
||||||
@@ -306,8 +284,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
}
|
}
|
||||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||||
}
|
}
|
||||||
} else if (externalNotificationModule->getMute()) {
|
} else if (isMuted) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||||
|
|
||||||
@@ -325,7 +303,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
int iconX = iconRightEdge - mute_symbol_width;
|
int iconX = iconRightEdge - mute_symbol_width;
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||||
|
|
||||||
if (isInverted && !force_no_invert) {
|
if (isInverted) {
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
@@ -383,8 +361,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||||
}
|
}
|
||||||
} else if (externalNotificationModule->getMute()) {
|
} else if (isMuted) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||||
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
||||||
@@ -403,7 +381,7 @@ const int *getTextPositions(OLEDDisplay *display)
|
|||||||
{
|
{
|
||||||
static int textPositions[7]; // Static array that persists beyond function scope
|
static int textPositions[7]; // Static array that persists beyond function scope
|
||||||
|
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
textPositions[0] = textZeroLine;
|
textPositions[0] = textZeroLine;
|
||||||
textPositions[1] = textFirstLine_medium;
|
textPositions[1] = textFirstLine_medium;
|
||||||
textPositions[2] = textSecondLine_medium;
|
textPositions[2] = textSecondLine_medium;
|
||||||
@@ -436,12 +414,8 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (drawConnectionState) {
|
if (drawConnectionState) {
|
||||||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
if (isHighResolution) {
|
||||||
display->setColor(BLACK);
|
const int scale = 2;
|
||||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
|
||||||
(connection_icon_height * scale) + (2 * scale));
|
|
||||||
display->setColor(WHITE);
|
|
||||||
if (currentResolution == ScreenResolution::High) {
|
|
||||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||||
int iconX = 0;
|
int iconX = 0;
|
||||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||||
@@ -470,49 +444,18 @@ bool isAllowedPunctuation(char c)
|
|||||||
return allowed.find(c) != std::string::npos;
|
return allowed.find(c) != std::string::npos;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void replaceAll(std::string &s, const std::string &from, const std::string &to)
|
|
||||||
{
|
|
||||||
if (from.empty())
|
|
||||||
return;
|
|
||||||
size_t pos = 0;
|
|
||||||
while ((pos = s.find(from, pos)) != std::string::npos) {
|
|
||||||
s.replace(pos, from.size(), to);
|
|
||||||
pos += to.size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string sanitizeString(const std::string &input)
|
std::string sanitizeString(const std::string &input)
|
||||||
{
|
{
|
||||||
std::string output;
|
std::string output;
|
||||||
bool inReplacement = false;
|
bool inReplacement = false;
|
||||||
|
|
||||||
// Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first.
|
for (char c : input) {
|
||||||
std::string s = input;
|
if (std::isalnum(static_cast<unsigned char>(c)) || isAllowedPunctuation(c)) {
|
||||||
|
|
||||||
// Curly single quotes: ‘ ’
|
|
||||||
replaceAll(s, "\xE2\x80\x98", "'"); // U+2018
|
|
||||||
replaceAll(s, "\xE2\x80\x99", "'"); // U+2019
|
|
||||||
|
|
||||||
// Curly double quotes: “ ”
|
|
||||||
replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C
|
|
||||||
replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D
|
|
||||||
|
|
||||||
// En dash / Em dash: – —
|
|
||||||
replaceAll(s, "\xE2\x80\x93", "-"); // U+2013
|
|
||||||
replaceAll(s, "\xE2\x80\x94", "-"); // U+2014
|
|
||||||
|
|
||||||
// Non-breaking space
|
|
||||||
replaceAll(s, "\xC2\xA0", " "); // U+00A0
|
|
||||||
|
|
||||||
// Now do your original sanitize pass over the normalized string.
|
|
||||||
for (unsigned char uc : s) {
|
|
||||||
char c = static_cast<char>(uc);
|
|
||||||
if (std::isalnum(uc) || isAllowedPunctuation(c)) {
|
|
||||||
output += c;
|
output += c;
|
||||||
inReplacement = false;
|
inReplacement = false;
|
||||||
} else {
|
} else {
|
||||||
if (!inReplacement) {
|
if (!inReplacement) {
|
||||||
output += static_cast<char>(0xBF); // ISO-8859-1 for inverted question mark
|
output += 0xbf; // ISO-8859-1 for inverted question mark
|
||||||
inReplacement = true;
|
inReplacement = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,11 +41,9 @@ namespace graphics
|
|||||||
|
|
||||||
// Shared state (declare inside namespace)
|
// Shared state (declare inside namespace)
|
||||||
extern bool hasUnreadMessage;
|
extern bool hasUnreadMessage;
|
||||||
enum class ScreenResolution : uint8_t { UltraLow = 0, Low = 1, High = 2 };
|
extern bool isMuted;
|
||||||
extern ScreenResolution currentResolution;
|
extern bool isHighResolution;
|
||||||
ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth);
|
void determineResolution(int16_t screenheight, int16_t screenwidth);
|
||||||
|
|
||||||
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second);
|
|
||||||
|
|
||||||
// Rounded highlight (used for inverted headers)
|
// Rounded highlight (used for inverted headers)
|
||||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
||||||
|
|||||||
@@ -354,6 +354,8 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16
|
|||||||
if (screenHeight <= 64) {
|
if (screenHeight <= 64) {
|
||||||
textY = boxY + (boxHeight - inputLineH) / 2;
|
textY = boxY + (boxHeight - inputLineH) / 2;
|
||||||
} else {
|
} else {
|
||||||
|
const int innerLeft = boxX + 1;
|
||||||
|
const int innerRight = boxX + boxWidth - 2;
|
||||||
const int innerTop = boxY + 1;
|
const int innerTop = boxY + 1;
|
||||||
const int innerBottom = boxY + boxHeight - 2;
|
const int innerBottom = boxY + boxHeight - 2;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
#include "ClockRenderer.h"
|
#include "ClockRenderer.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "UIRenderer.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "gps/GeoCoord.h"
|
||||||
#include "gps/RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "graphics/draw/UIRenderer.h"
|
#include "graphics/draw/UIRenderer.h"
|
||||||
|
#include "graphics/emotes.h"
|
||||||
#include "graphics/images.h"
|
#include "graphics/images.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
|
||||||
@@ -18,31 +23,6 @@ namespace graphics
|
|||||||
namespace ClockRenderer
|
namespace ClockRenderer
|
||||||
{
|
{
|
||||||
|
|
||||||
// Segment bitmaps for numerals 0-9 stored in flash to save RAM.
|
|
||||||
// Each row is a digit, each column is a segment state (1 = on, 0 = off).
|
|
||||||
// Segment layout reference:
|
|
||||||
//
|
|
||||||
// ___1___
|
|
||||||
// 6 | | 2
|
|
||||||
// |_7___|
|
|
||||||
// 5 | | 3
|
|
||||||
// |___4_|
|
|
||||||
//
|
|
||||||
// Segment order: [1, 2, 3, 4, 5, 6, 7]
|
|
||||||
//
|
|
||||||
static const uint8_t PROGMEM digitSegments[10][7] = {
|
|
||||||
{1, 1, 1, 1, 1, 1, 0}, // 0
|
|
||||||
{0, 1, 1, 0, 0, 0, 0}, // 1
|
|
||||||
{1, 1, 0, 1, 1, 0, 1}, // 2
|
|
||||||
{1, 1, 1, 1, 0, 0, 1}, // 3
|
|
||||||
{0, 1, 1, 0, 0, 1, 1}, // 4
|
|
||||||
{1, 0, 1, 1, 0, 1, 1}, // 5
|
|
||||||
{1, 0, 1, 1, 1, 1, 1}, // 6
|
|
||||||
{1, 1, 1, 0, 0, 1, 0}, // 7
|
|
||||||
{1, 1, 1, 1, 1, 1, 1}, // 8
|
|
||||||
{1, 1, 1, 1, 0, 1, 1} // 9
|
|
||||||
};
|
|
||||||
|
|
||||||
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
||||||
{
|
{
|
||||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||||
@@ -50,7 +30,7 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
|||||||
|
|
||||||
uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8;
|
uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8;
|
||||||
|
|
||||||
uint16_t topAndBottomX = x + static_cast<uint16_t>(4 * scale);
|
uint16_t topAndBottomX = x + (4 * scale);
|
||||||
|
|
||||||
uint16_t quarterCellHeight = cellHeight / 4;
|
uint16_t quarterCellHeight = cellHeight / 4;
|
||||||
|
|
||||||
@@ -63,16 +43,34 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
|||||||
|
|
||||||
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale)
|
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale)
|
||||||
{
|
{
|
||||||
// Read 7-segment pattern for the digit from flash
|
// the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of
|
||||||
uint8_t seg[7];
|
// segment {innerIndex + 1}
|
||||||
for (uint8_t i = 0; i < 7; i++) {
|
// e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off.
|
||||||
seg[i] = pgm_read_byte(&digitSegments[number][i]);
|
uint8_t numbers[10][7] = {
|
||||||
}
|
{1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key
|
||||||
|
{0, 1, 1, 0, 0, 0, 0}, // 1 1
|
||||||
|
{1, 1, 0, 1, 1, 0, 1}, // 2 ___
|
||||||
|
{1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2
|
||||||
|
{0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_|
|
||||||
|
{1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3
|
||||||
|
{1, 0, 1, 1, 1, 1, 1}, // 6 |___|
|
||||||
|
{1, 1, 1, 0, 0, 1, 0}, // 7
|
||||||
|
{1, 1, 1, 1, 1, 1, 1}, // 8 4
|
||||||
|
{1, 1, 1, 1, 0, 1, 1}, // 9
|
||||||
|
};
|
||||||
|
|
||||||
|
// the width and height of each segment's central rectangle:
|
||||||
|
// _____________________
|
||||||
|
// ⋰| (only this part, |⋱
|
||||||
|
// ⋰ | not including | ⋱
|
||||||
|
// ⋱ | the triangles | ⋰
|
||||||
|
// ⋱| on the ends) |⋰
|
||||||
|
// ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||||
|
|
||||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||||
|
|
||||||
// Precompute segment positions
|
// segment x and y coordinates
|
||||||
uint16_t segmentOneX = x + segmentHeight + 2;
|
uint16_t segmentOneX = x + segmentHeight + 2;
|
||||||
uint16_t segmentOneY = y;
|
uint16_t segmentOneY = y;
|
||||||
|
|
||||||
@@ -94,21 +92,33 @@ void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t n
|
|||||||
uint16_t segmentSevenX = segmentOneX;
|
uint16_t segmentSevenX = segmentOneX;
|
||||||
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
|
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
|
||||||
|
|
||||||
// Draw only the active segments
|
if (numbers[number][0]) {
|
||||||
if (seg[0])
|
graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||||
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
}
|
||||||
if (seg[1])
|
|
||||||
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
if (numbers[number][1]) {
|
||||||
if (seg[2])
|
graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||||
drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
}
|
||||||
if (seg[3])
|
|
||||||
drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
if (numbers[number][2]) {
|
||||||
if (seg[4])
|
graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||||
drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
}
|
||||||
if (seg[5])
|
|
||||||
drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
if (numbers[number][3]) {
|
||||||
if (seg[6])
|
graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||||
drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
|
}
|
||||||
|
|
||||||
|
if (numbers[number][4]) {
|
||||||
|
graphics::ClockRenderer::drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numbers[number][5]) {
|
||||||
|
graphics::ClockRenderer::drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numbers[number][6]) {
|
||||||
|
graphics::ClockRenderer::drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height)
|
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height)
|
||||||
@@ -137,6 +147,42 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig
|
|||||||
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
|
||||||
|
{
|
||||||
|
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||||
|
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||||
|
|
||||||
|
if (digitalMode) {
|
||||||
|
uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2;
|
||||||
|
uint16_t centerX = (x + segmentHeight + 2) + (radius / 2);
|
||||||
|
uint16_t centerY = (y + segmentHeight + 2) + (radius / 2);
|
||||||
|
|
||||||
|
display->drawCircle(centerX, centerY, radius);
|
||||||
|
display->drawCircle(centerX, centerY, radius + 1);
|
||||||
|
display->drawLine(centerX, centerY, centerX, centerY - radius + 3);
|
||||||
|
display->drawLine(centerX, centerY, centerX + radius - 3, centerY);
|
||||||
|
} else {
|
||||||
|
uint16_t segmentOneX = x + segmentHeight + 2;
|
||||||
|
uint16_t segmentOneY = y;
|
||||||
|
|
||||||
|
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
|
||||||
|
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
|
||||||
|
|
||||||
|
uint16_t segmentThreeX = segmentOneX;
|
||||||
|
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2;
|
||||||
|
|
||||||
|
uint16_t segmentFourX = x;
|
||||||
|
uint16_t segmentFourY = y + segmentHeight + 2;
|
||||||
|
|
||||||
|
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||||
|
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||||
|
drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||||
|
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// Draw a digital clock
|
||||||
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
display->clear();
|
display->clear();
|
||||||
@@ -146,6 +192,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
const char *titleStr = "";
|
const char *titleStr = "";
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||||
|
int line = 0;
|
||||||
|
|
||||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||||
char timeString[16];
|
char timeString[16];
|
||||||
@@ -190,7 +237,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
float target_width = display->getWidth() * screenwidth_target_ratio;
|
float target_width = display->getWidth() * screenwidth_target_ratio;
|
||||||
float target_height =
|
float target_height =
|
||||||
display->getHeight() -
|
display->getHeight() -
|
||||||
((currentResolution == ScreenResolution::High)
|
(isHighResolution
|
||||||
? 46
|
? 46
|
||||||
: 33); // Be careful adjusting this number, we have to account for header and the text under the time
|
: 33); // Be careful adjusting this number, we have to account for header and the text under the time
|
||||||
|
|
||||||
@@ -221,9 +268,10 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
scaleInitialized = true;
|
scaleInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate hours:minutes string width
|
|
||||||
size_t len = strlen(timeString);
|
size_t len = strlen(timeString);
|
||||||
uint16_t timeStringWidth = len * 5;
|
|
||||||
|
// calculate hours:minutes string width
|
||||||
|
uint16_t timeStringWidth = len * 5; // base spacing between characters
|
||||||
|
|
||||||
for (size_t i = 0; i < len; i++) {
|
for (size_t i = 0; i < len; i++) {
|
||||||
char character = timeString[i];
|
char character = timeString[i];
|
||||||
@@ -262,16 +310,9 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
|
|
||||||
// draw seconds string + AM/PM
|
// draw seconds string + AM/PM
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
int xOffset = -1;
|
int xOffset = (isHighResolution) ? 0 : -1;
|
||||||
if (currentResolution == ScreenResolution::High) {
|
|
||||||
xOffset = 0;
|
|
||||||
}
|
|
||||||
if (hour >= 10) {
|
if (hour >= 10) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
xOffset += (isHighResolution) ? 32 : 18;
|
||||||
xOffset += 32;
|
|
||||||
} else {
|
|
||||||
xOffset += 18;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.display.use_12h_clock) {
|
if (config.display.use_12h_clock) {
|
||||||
@@ -279,7 +320,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10;
|
xOffset = (isHighResolution) ? 18 : 10;
|
||||||
if (scale >= 2.0f) {
|
if (scale >= 2.0f) {
|
||||||
xOffset -= (int)(4.5f * scale);
|
xOffset -= (int)(4.5f * scale);
|
||||||
}
|
}
|
||||||
@@ -298,13 +339,19 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
const char *titleStr = "";
|
const char *titleStr = "";
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||||
|
int line = 0;
|
||||||
|
|
||||||
// clock face center coordinates
|
// clock face center coordinates
|
||||||
int16_t centerX = display->getWidth() / 2;
|
int16_t centerX = display->getWidth() / 2;
|
||||||
int16_t centerY = display->getHeight() / 2;
|
int16_t centerY = display->getHeight() / 2;
|
||||||
|
|
||||||
// clock face radius
|
// clock face radius
|
||||||
int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9;
|
int16_t radius = 0;
|
||||||
|
if (display->getHeight() < display->getWidth()) {
|
||||||
|
radius = (display->getHeight() / 2) * 0.9;
|
||||||
|
} else {
|
||||||
|
radius = (display->getWidth() / 2) * 0.9;
|
||||||
|
}
|
||||||
#ifdef T_WATCH_S3
|
#ifdef T_WATCH_S3
|
||||||
radius = (display->getWidth() / 2) * 0.8;
|
radius = (display->getWidth() / 2) * 0.8;
|
||||||
#endif
|
#endif
|
||||||
@@ -319,8 +366,17 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
// tick mark outer y coordinate; (first nested circle)
|
// tick mark outer y coordinate; (first nested circle)
|
||||||
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
||||||
|
|
||||||
double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4);
|
// seconds tick mark inner y coordinate; (second nested circle)
|
||||||
double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6);
|
double secondsTickMarkInnerNoonY = (double)noonY + 4;
|
||||||
|
if (isHighResolution) {
|
||||||
|
secondsTickMarkInnerNoonY = (double)noonY + 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hours tick mark inner y coordinate; (third nested circle)
|
||||||
|
double hoursTickMarkInnerNoonY = (double)noonY + 6;
|
||||||
|
if (isHighResolution) {
|
||||||
|
hoursTickMarkInnerNoonY = (double)noonY + 16;
|
||||||
|
}
|
||||||
|
|
||||||
// minute hand y coordinate
|
// minute hand y coordinate
|
||||||
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
||||||
@@ -330,7 +386,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
|
|
||||||
// hour hand radius and y coordinate
|
// hour hand radius and y coordinate
|
||||||
int16_t hourHandRadius = radius * 0.35;
|
int16_t hourHandRadius = radius * 0.35;
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
hourHandRadius = radius * 0.55;
|
hourHandRadius = radius * 0.55;
|
||||||
}
|
}
|
||||||
int16_t hourHandNoonY = centerY - hourHandRadius;
|
int16_t hourHandNoonY = centerY - hourHandRadius;
|
||||||
@@ -340,13 +396,19 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
|
|
||||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||||
if (rtc_sec > 0) {
|
if (rtc_sec > 0) {
|
||||||
int hour, minute, second;
|
long hms = rtc_sec % SEC_PER_DAY;
|
||||||
decomposeTime(rtc_sec, hour, minute, second);
|
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||||
|
|
||||||
|
// Tear apart hms into h:m:s
|
||||||
|
int hour = hms / SEC_PER_HOUR;
|
||||||
|
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
|
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||||
|
|
||||||
|
bool isPM = hour >= 12;
|
||||||
if (config.display.use_12h_clock) {
|
if (config.display.use_12h_clock) {
|
||||||
bool isPM = hour >= 12;
|
isPM = hour >= 12;
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0;
|
int yOffset = isHighResolution ? 1 : 0;
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
yOffset += 3;
|
yOffset += 3;
|
||||||
#endif
|
#endif
|
||||||
@@ -437,13 +499,12 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
#else
|
#else
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
// draw hour number
|
// draw hour number
|
||||||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
if (currentResolution == ScreenResolution::High &&
|
if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
|
||||||
(hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
|
|
||||||
// draw hour number
|
// draw hour number
|
||||||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
}
|
}
|
||||||
@@ -455,7 +516,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
|
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
|
||||||
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
|
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
|
||||||
|
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
// draw minute tick mark
|
// draw minute tick mark
|
||||||
display->drawLine(startX, startY, endX, endY);
|
display->drawLine(startX, startY, endX, endY);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
|
|||||||
// This could draw a "N" indicator or north arrow
|
// This could draw a "N" indicator or north arrow
|
||||||
// For now, we'll draw a simple north indicator
|
// For now, we'll draw a simple north indicator
|
||||||
// const float radius = 17.0f;
|
// const float radius = 17.0f;
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
radius += 4;
|
radius += 4;
|
||||||
}
|
}
|
||||||
Point north(0, -radius);
|
Point north(0, -radius);
|
||||||
@@ -59,7 +59,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
|
|||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
||||||
} else {
|
} else {
|
||||||
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
||||||
|
|||||||
@@ -282,13 +282,13 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
|||||||
std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds);
|
std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds);
|
||||||
|
|
||||||
// Line 1 (Still)
|
// Line 1 (Still)
|
||||||
if (currentResolution != graphics::ScreenResolution::UltraLow) {
|
#if !defined(M5STACK_UNITC6L)
|
||||||
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
|
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
|
||||||
if (config.display.heading_bold)
|
if (config.display.heading_bold)
|
||||||
display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
|
display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
|
||||||
|
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
}
|
#endif
|
||||||
// Setup string to assemble analogClock string
|
// Setup string to assemble analogClock string
|
||||||
std::string analogClock = "";
|
std::string analogClock = "";
|
||||||
|
|
||||||
@@ -301,8 +301,9 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
|||||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||||
|
|
||||||
// Tear apart hms into h:m:s
|
// Tear apart hms into h:m:s
|
||||||
int hour, min, sec;
|
int hour = hms / SEC_PER_HOUR;
|
||||||
graphics::decomposeTime(rtc_sec, hour, min, sec);
|
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
|
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||||
|
|
||||||
char timebuf[12];
|
char timebuf[12];
|
||||||
|
|
||||||
@@ -378,7 +379,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
int line = 1;
|
int line = 1;
|
||||||
|
|
||||||
// === Set Title
|
// === Set Title
|
||||||
const char *titleStr = (currentResolution == ScreenResolution::High) ? "LoRa Info" : "LoRa";
|
const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa";
|
||||||
|
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
@@ -390,11 +391,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
char shortnameble[35];
|
char shortnameble[35];
|
||||||
getMacAddr(dmac);
|
getMacAddr(dmac);
|
||||||
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
#if defined(M5STACK_UNITC6L)
|
||||||
snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId);
|
snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId);
|
||||||
} else {
|
#else
|
||||||
snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
|
snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
|
||||||
}
|
#endif
|
||||||
int textWidth = display->getStringWidth(shortnameble);
|
int textWidth = display->getStringWidth(shortnameble);
|
||||||
int nameX = (SCREEN_WIDTH - textWidth);
|
int nameX = (SCREEN_WIDTH - textWidth);
|
||||||
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
|
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
|
||||||
@@ -413,11 +414,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
char regionradiopreset[25];
|
char regionradiopreset[25];
|
||||||
const char *region = myRegion ? myRegion->name : NULL;
|
const char *region = myRegion ? myRegion->name : NULL;
|
||||||
if (region != nullptr) {
|
if (region != nullptr) {
|
||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
#if defined(M5STACK_UNITC6L)
|
||||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
|
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
|
||||||
} else {
|
#else
|
||||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
|
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
|
||||||
}
|
#endif
|
||||||
}
|
}
|
||||||
textWidth = display->getStringWidth(regionradiopreset);
|
textWidth = display->getStringWidth(regionradiopreset);
|
||||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
@@ -429,17 +430,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
float freq = RadioLibInterface::instance->getFreq();
|
float freq = RadioLibInterface::instance->getFreq();
|
||||||
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
|
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
|
||||||
if (config.lora.channel_num == 0) {
|
if (config.lora.channel_num == 0) {
|
||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
#if defined(M5STACK_UNITC6L)
|
||||||
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr);
|
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr);
|
||||||
} else {
|
#else
|
||||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
|
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
|
||||||
}
|
#endif
|
||||||
} else {
|
} else {
|
||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
#if defined(M5STACK_UNITC6L)
|
||||||
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
|
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
|
||||||
} else {
|
#else
|
||||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz (%d)", freqStr, config.lora.channel_num);
|
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
|
||||||
}
|
#endif
|
||||||
}
|
}
|
||||||
size_t len = strlen(frequencyslot);
|
size_t len = strlen(frequencyslot);
|
||||||
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
|
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
|
||||||
@@ -455,13 +456,12 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
char chUtilPercentage[10];
|
char chUtilPercentage[10];
|
||||||
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
||||||
|
|
||||||
int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10
|
int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
|
||||||
: display->getStringWidth(chUtil) + 5;
|
|
||||||
int chUtil_y = getTextPositions(display)[line] + 3;
|
int chUtil_y = getTextPositions(display)[line] + 3;
|
||||||
|
|
||||||
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50;
|
int chutil_bar_width = (isHighResolution) ? 100 : 50;
|
||||||
int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7;
|
int chutil_bar_height = (isHighResolution) ? 12 : 7;
|
||||||
int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3;
|
int extraoffset = (isHighResolution) ? 6 : 3;
|
||||||
int chutil_percent = airTime->channelUtilizationPercent();
|
int chutil_percent = airTime->channelUtilizationPercent();
|
||||||
|
|
||||||
int centerofscreen = SCREEN_WIDTH / 2;
|
int centerofscreen = SCREEN_WIDTH / 2;
|
||||||
@@ -530,18 +530,15 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
|||||||
int line = 1;
|
int line = 1;
|
||||||
const int barHeight = 6;
|
const int barHeight = 6;
|
||||||
const int labelX = x;
|
const int labelX = x;
|
||||||
int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0;
|
int barsOffset = (isHighResolution) ? 24 : 0;
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
#ifndef T_DECK_PRO
|
|
||||||
barsOffset -= 12;
|
barsOffset -= 12;
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(M5STACK_UNITC6L)
|
||||||
|
const int barX = x + 45 + barsOffset;
|
||||||
|
#else
|
||||||
|
const int barX = x + 40 + barsOffset;
|
||||||
#endif
|
#endif
|
||||||
int barX = x + barsOffset;
|
|
||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
|
||||||
barX += 45;
|
|
||||||
} else {
|
|
||||||
barX += 40;
|
|
||||||
}
|
|
||||||
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
|
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
|
||||||
if (total == 0)
|
if (total == 0)
|
||||||
return;
|
return;
|
||||||
@@ -549,7 +546,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
|||||||
int percent = (used * 100) / total;
|
int percent = (used * 100) / total;
|
||||||
|
|
||||||
char combinedStr[24];
|
char combinedStr[24];
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
|
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
|
||||||
total / 1024);
|
total / 1024);
|
||||||
} else {
|
} else {
|
||||||
@@ -577,7 +574,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
|||||||
#endif
|
#endif
|
||||||
// Value string
|
// Value string
|
||||||
display->setTextAlignment(TEXT_ALIGN_RIGHT);
|
display->setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||||
display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr);
|
display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr);
|
||||||
};
|
};
|
||||||
|
|
||||||
// === Memory values ===
|
// === Memory values ===
|
||||||
@@ -629,33 +626,25 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
|||||||
line += 1;
|
line += 1;
|
||||||
|
|
||||||
char appversionstr[35];
|
char appversionstr[35];
|
||||||
|
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION));
|
||||||
char appversionstr_formatted[40];
|
char appversionstr_formatted[40];
|
||||||
|
char *lastDot = strrchr(appversionstr, '.');
|
||||||
const char *ver = optstr(APP_VERSION);
|
#if defined(M5STACK_UNITC6L)
|
||||||
char verbuf[32];
|
if (lastDot != nullptr) {
|
||||||
strncpy(verbuf, ver, sizeof(verbuf) - 1);
|
*lastDot = '\0'; // truncate string
|
||||||
verbuf[sizeof(verbuf) - 1] = '\0';
|
|
||||||
|
|
||||||
char *lastDot = strrchr(verbuf, '.');
|
|
||||||
|
|
||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
|
||||||
if (lastDot != nullptr) {
|
|
||||||
*lastDot = '\0';
|
|
||||||
}
|
|
||||||
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf);
|
|
||||||
} else {
|
|
||||||
if (lastDot) {
|
|
||||||
size_t prefixLen = (size_t)(lastDot - verbuf);
|
|
||||||
snprintf(appversionstr_formatted, sizeof(appversionstr_formatted), "Ver: %.*s", (int)prefixLen, verbuf);
|
|
||||||
strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
|
|
||||||
strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
|
|
||||||
strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
|
|
||||||
strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1);
|
|
||||||
appversionstr[sizeof(appversionstr) - 1] = '\0';
|
|
||||||
} else {
|
|
||||||
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (lastDot) {
|
||||||
|
size_t prefixLen = lastDot - appversionstr;
|
||||||
|
strncpy(appversionstr_formatted, appversionstr, prefixLen);
|
||||||
|
appversionstr_formatted[prefixLen] = '\0';
|
||||||
|
strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
|
||||||
|
strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
|
||||||
|
strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
|
||||||
|
strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1);
|
||||||
|
appversionstr[sizeof(appversionstr) - 1] = '\0';
|
||||||
|
}
|
||||||
|
#endif
|
||||||
int textWidth = display->getStringWidth(appversionstr);
|
int textWidth = display->getStringWidth(appversionstr);
|
||||||
int nameX = (SCREEN_WIDTH - textWidth) / 2;
|
int nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
|
|
||||||
@@ -674,7 +663,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
|||||||
const char *clientWord = nullptr;
|
const char *clientWord = nullptr;
|
||||||
|
|
||||||
// Determine if narrow or wide screen
|
// Determine if narrow or wide screen
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
clientWord = "Client";
|
clientWord = "Client";
|
||||||
} else {
|
} else {
|
||||||
clientWord = "App";
|
clientWord = "App";
|
||||||
@@ -715,23 +704,11 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
|||||||
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
|
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
|
||||||
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
||||||
int textX_offset = 10;
|
int textX_offset = 10;
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (isHighResolution) {
|
||||||
|
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
|
||||||
|
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
|
||||||
textX_offset = textX_offset * 4;
|
textX_offset = textX_offset * 4;
|
||||||
const int scale = 2;
|
display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
|
||||||
const int bytesPerRow = (chirpy_width + 7) / 8;
|
|
||||||
|
|
||||||
for (int yy = 0; yy < chirpy_height; ++yy) {
|
|
||||||
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
|
|
||||||
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
|
|
||||||
const uint8_t *rowPtr = chirpy + yy * bytesPerRow;
|
|
||||||
for (int xx = 0; xx < chirpy_width; ++xx) {
|
|
||||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
|
||||||
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
|
||||||
if (byteVal & bitMask) {
|
|
||||||
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
|
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "graphics/draw/CompassRenderer.h"
|
#include "graphics/draw/CompassRenderer.h"
|
||||||
#include "graphics/draw/DebugRenderer.h"
|
#include "graphics/draw/DebugRenderer.h"
|
||||||
#include "graphics/draw/NodeListRenderer.h"
|
#include "graphics/draw/NodeListRenderer.h"
|
||||||
|
#include "graphics/draw/ScreenRenderer.h"
|
||||||
#include "graphics/draw/UIRenderer.h"
|
#include "graphics/draw/UIRenderer.h"
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
@@ -29,6 +30,8 @@ using namespace ClockRenderer;
|
|||||||
using namespace CompassRenderer;
|
using namespace CompassRenderer;
|
||||||
using namespace DebugRenderer;
|
using namespace DebugRenderer;
|
||||||
using namespace NodeListRenderer;
|
using namespace NodeListRenderer;
|
||||||
|
using namespace ScreenRenderer;
|
||||||
|
using namespace UIRenderer;
|
||||||
|
|
||||||
} // namespace DrawRenderers
|
} // namespace DrawRenderers
|
||||||
|
|
||||||
|
|||||||
+653
-1652
File diff suppressed because it is too large
Load Diff
+46
-107
@@ -8,74 +8,59 @@ class menuHandler
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum screenMenus {
|
enum screenMenus {
|
||||||
MenuNone,
|
menu_none,
|
||||||
LoraMenu,
|
lora_Menu,
|
||||||
LoraPicker,
|
lora_picker,
|
||||||
DeviceRolePicker,
|
device_role_picker,
|
||||||
RadioPresetPicker,
|
radio_preset_picker,
|
||||||
FrequencySlot,
|
no_timeout_lora_picker,
|
||||||
NoTimeoutLoraPicker,
|
TZ_picker,
|
||||||
TzPicker,
|
twelve_hour_picker,
|
||||||
TwelveHourPicker,
|
clock_face_picker,
|
||||||
ClockFacePicker,
|
clock_menu,
|
||||||
ClockMenu,
|
position_base_menu,
|
||||||
PositionBaseMenu,
|
gps_toggle_menu,
|
||||||
NodeBaseMenu,
|
gps_format_menu,
|
||||||
GpsToggleMenu,
|
compass_point_north_menu,
|
||||||
GpsFormatMenu,
|
reset_node_db_menu,
|
||||||
GpsSmartPositionMenu,
|
buzzermodemenupicker,
|
||||||
GpsUpdateIntervalMenu,
|
mui_picker,
|
||||||
GpsPositionBroadcastMenu,
|
tftcolormenupicker,
|
||||||
CompassPointNorthMenu,
|
brightness_picker,
|
||||||
ResetNodeDbMenu,
|
reboot_menu,
|
||||||
BuzzerModeMenuPicker,
|
shutdown_menu,
|
||||||
MuiPicker,
|
add_favorite,
|
||||||
TftColorMenuPicker,
|
remove_favorite,
|
||||||
BrightnessPicker,
|
test_menu,
|
||||||
RebootMenu,
|
number_test,
|
||||||
ShutdownMenu,
|
wifi_toggle_menu,
|
||||||
NodePickerMenu,
|
bluetooth_toggle_menu,
|
||||||
ManageNodeMenu,
|
notifications_menu,
|
||||||
RemoveFavorite,
|
screen_options_menu,
|
||||||
TestMenu,
|
power_menu,
|
||||||
NumberTest,
|
system_base_menu,
|
||||||
WifiToggleMenu,
|
key_verification_init,
|
||||||
BluetoothToggleMenu,
|
key_verification_final_prompt,
|
||||||
ScreenOptionsMenu,
|
trace_route_menu,
|
||||||
PowerMenu,
|
throttle_message,
|
||||||
SystemBaseMenu,
|
node_name_length_menu,
|
||||||
KeyVerificationInit,
|
|
||||||
KeyVerificationFinalPrompt,
|
|
||||||
TraceRouteMenu,
|
|
||||||
ThrottleMessage,
|
|
||||||
MessageResponseMenu,
|
|
||||||
MessageViewModeMenu,
|
|
||||||
ReplyMenu,
|
|
||||||
DeleteMessagesMenu,
|
|
||||||
NodeNameLengthMenu,
|
|
||||||
FrameToggles,
|
FrameToggles,
|
||||||
DisplayUnits,
|
DisplayUnits
|
||||||
MessageBubblesMenu
|
|
||||||
};
|
};
|
||||||
static screenMenus menuQueue;
|
static screenMenus menuQueue;
|
||||||
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
|
||||||
|
|
||||||
static void OnboardMessage();
|
static void OnboardMessage();
|
||||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||||
static void loraMenu();
|
static void loraMenu();
|
||||||
static void deviceRolePicker();
|
static void DeviceRolePicker();
|
||||||
static void radioPresetPicker();
|
static void RadioPresetPicker();
|
||||||
static void FrequencySlotPicker();
|
|
||||||
static void handleMenuSwitch(OLEDDisplay *display);
|
static void handleMenuSwitch(OLEDDisplay *display);
|
||||||
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
||||||
static void clockMenu();
|
static void clockMenu();
|
||||||
static void TZPicker();
|
static void TZPicker();
|
||||||
static void twelveHourPicker();
|
static void TwelveHourPicker();
|
||||||
static void clockFacePicker();
|
static void ClockFacePicker();
|
||||||
static void messageResponseMenu();
|
static void messageResponseMenu();
|
||||||
static void messageViewModeMenu();
|
|
||||||
static void replyMenu();
|
|
||||||
static void deleteMessagesMenu();
|
|
||||||
static void homeBaseMenu();
|
static void homeBaseMenu();
|
||||||
static void textMessageBaseMenu();
|
static void textMessageBaseMenu();
|
||||||
static void systemBaseMenu();
|
static void systemBaseMenu();
|
||||||
@@ -84,9 +69,6 @@ class menuHandler
|
|||||||
static void compassNorthMenu();
|
static void compassNorthMenu();
|
||||||
static void GPSToggleMenu();
|
static void GPSToggleMenu();
|
||||||
static void GPSFormatMenu();
|
static void GPSFormatMenu();
|
||||||
static void GPSSmartPositionMenu();
|
|
||||||
static void GPSUpdateIntervalMenu();
|
|
||||||
static void GPSPositionBroadcastMenu();
|
|
||||||
static void BuzzerModeMenu();
|
static void BuzzerModeMenu();
|
||||||
static void switchToMUIMenu();
|
static void switchToMUIMenu();
|
||||||
static void TFTColorPickerMenu(OLEDDisplay *display);
|
static void TFTColorPickerMenu(OLEDDisplay *display);
|
||||||
@@ -95,8 +77,6 @@ class menuHandler
|
|||||||
static void BrightnessPickerMenu();
|
static void BrightnessPickerMenu();
|
||||||
static void rebootMenu();
|
static void rebootMenu();
|
||||||
static void shutdownMenu();
|
static void shutdownMenu();
|
||||||
static void NodePicker();
|
|
||||||
static void manageNodeMenu();
|
|
||||||
static void addFavoriteMenu();
|
static void addFavoriteMenu();
|
||||||
static void removeFavoriteMenu();
|
static void removeFavoriteMenu();
|
||||||
static void traceRouteMenu();
|
static void traceRouteMenu();
|
||||||
@@ -104,61 +84,20 @@ class menuHandler
|
|||||||
static void numberTest();
|
static void numberTest();
|
||||||
static void wifiBaseMenu();
|
static void wifiBaseMenu();
|
||||||
static void wifiToggleMenu();
|
static void wifiToggleMenu();
|
||||||
|
static void notificationsMenu();
|
||||||
static void screenOptionsMenu();
|
static void screenOptionsMenu();
|
||||||
static void powerMenu();
|
static void powerMenu();
|
||||||
static void nodeNameLengthMenu();
|
static void nodeNameLengthMenu();
|
||||||
static void frameTogglesMenu();
|
static void FrameToggles_menu();
|
||||||
static void displayUnitsMenu();
|
static void DisplayUnits_menu();
|
||||||
static void messageBubblesMenu();
|
|
||||||
static void textMessageMenu();
|
static void textMessageMenu();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void saveUIConfig();
|
static void saveUIConfig();
|
||||||
static void keyVerificationInitMenu();
|
static void keyVerificationInitMenu();
|
||||||
static void keyVerificationFinalPrompt();
|
static void keyVerificationFinalPrompt();
|
||||||
static void bluetoothToggleMenu();
|
static void BluetoothToggleMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Generic Menu Options designations */
|
|
||||||
enum class OptionsAction { Back, Select };
|
|
||||||
|
|
||||||
template <typename T> struct MenuOption {
|
|
||||||
const char *label;
|
|
||||||
OptionsAction action;
|
|
||||||
bool hasValue;
|
|
||||||
T value;
|
|
||||||
|
|
||||||
MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn)
|
|
||||||
: label(labelIn), action(actionIn), hasValue(true), value(valueIn)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ScreenColor {
|
|
||||||
uint8_t r;
|
|
||||||
uint8_t g;
|
|
||||||
uint8_t b;
|
|
||||||
bool useVariant;
|
|
||||||
|
|
||||||
explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
|
|
||||||
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
using RadioPresetOption = MenuOption<meshtastic_Config_LoRaConfig_ModemPreset>;
|
|
||||||
using LoraRegionOption = MenuOption<meshtastic_Config_LoRaConfig_RegionCode>;
|
|
||||||
using TimezoneOption = MenuOption<const char *>;
|
|
||||||
using CompassOption = MenuOption<meshtastic_CompassMode>;
|
|
||||||
using ScreenColorOption = MenuOption<ScreenColor>;
|
|
||||||
using GPSToggleOption = MenuOption<meshtastic_Config_PositionConfig_GpsMode>;
|
|
||||||
using GPSFormatOption = MenuOption<meshtastic_DeviceUIConfig_GpsCoordinateFormat>;
|
|
||||||
using NodeNameOption = MenuOption<bool>;
|
|
||||||
using PositionMenuOption = MenuOption<int>;
|
|
||||||
using ManageNodeOption = MenuOption<int>;
|
|
||||||
using ClockFaceOption = MenuOption<bool>;
|
|
||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
#endif
|
#endif
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user