mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-22 11:17:49 +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 libsdl2-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.501
|
- checkov@3.2.495
|
||||||
- renovate@43.10.3
|
- renovate@42.30.4
|
||||||
- prettier@3.8.1
|
- prettier@3.7.4
|
||||||
- trufflehog@3.93.3
|
- 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.1
|
- 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
|
||||||
|
|||||||
+2
-2
@@ -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 libsdl2-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
|
||||||
@@ -53,7 +53,7 @@ USER root
|
|||||||
RUN apt-get update && apt-get --no-install-recommends -y install \
|
RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||||
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||||
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
||||||
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
|
libx11-6 libinput10 libxkbcommon-x11-0 \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
&& mkdir -p /var/lib/meshtasticd \
|
&& mkdir -p /var/lib/meshtasticd \
|
||||||
&& mkdir -p /etc/meshtasticd/config.d \
|
&& mkdir -p /etc/meshtasticd/config.d \
|
||||||
|
|||||||
+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
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -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 sdl2-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
|
||||||
@@ -42,7 +42,7 @@ USER root
|
|||||||
|
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
|
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
|
||||||
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
|
i2c-tools libuv libx11 libinput libxkbcommon \
|
||||||
&& rm -rf /var/cache/apk/* \
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& mkdir -p /var/lib/meshtasticd \
|
&& mkdir -p /var/lib/meshtasticd \
|
||||||
&& mkdir -p /etc/meshtasticd/config.d \
|
&& mkdir -p /etc/meshtasticd/config.d \
|
||||||
|
|||||||
+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.
|
||||||
|
|||||||
+13
-20
@@ -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,7 +83,7 @@ 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
|
||||||
|
|
||||||
@@ -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,15 +87,6 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
<release version="2.7.20" date="2026-02-11">
|
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20</url>
|
|
||||||
</release>
|
|
||||||
<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>
|
||||||
|
|||||||
+13
-164
@@ -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(
|
||||||
# For host/native envs, avoid depending on 'buildprog' (some targets don't define it)
|
f'mv $BUILD_DIR/littlefs.bin $BUILD_DIR/{lfsbin}',
|
||||||
mtjson_deps = [] if should_skip_manifest else ["buildprog"]
|
f'Renaming littlefs.bin to {lfsbin}'
|
||||||
if not should_skip_manifest and platform.name == "espressif32":
|
))
|
||||||
# Build littlefs image as part of mtjson target
|
|
||||||
# Equivalent to `pio run -t buildfs`
|
|
||||||
target_lfs = env.DataToBin(
|
|
||||||
join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR"
|
|
||||||
)
|
|
||||||
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(
|
env.AddCustomTarget(
|
||||||
name="mtjson",
|
name="mtjson",
|
||||||
dependencies=mtjson_deps,
|
dependencies=None,
|
||||||
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],
|
actions=[manifest_gather],
|
||||||
title="Meshtastic Manifest",
|
title="Meshtastic Manifest",
|
||||||
description="Generating Meshtastic manifest JSON + Checksums",
|
description="Generating Meshtastic manifest JSON + Checksums",
|
||||||
always_build=True,
|
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"
|
|
||||||
}
|
|
||||||
@@ -32,8 +32,7 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd",
|
"svd_path": "nrf52840.svd"
|
||||||
"openocd_target": "nrf52840-mdk-rs"
|
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "Minesemi ME25LS01",
|
"name": "Minesemi ME25LS01",
|
||||||
|
|||||||
@@ -33,8 +33,7 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd",
|
"svd_path": "nrf52840.svd"
|
||||||
"openocd_target": "nrf52840-mdk-rs"
|
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "MeshLink",
|
"name": "MeshLink",
|
||||||
|
|||||||
@@ -1,51 +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",
|
|
||||||
"openocd_target": "nrf52840-mdk-rs"
|
|
||||||
},
|
|
||||||
"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
-2
@@ -32,8 +32,7 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd",
|
"svd_path": "nrf52840.svd"
|
||||||
"openocd_target": "nrf52840-mdk-rs"
|
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "MINEWSEMI_MS24SF1_SX1262",
|
"name": "MINEWSEMI_MS24SF1_SX1262",
|
||||||
|
|||||||
@@ -32,8 +32,7 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd",
|
"svd_path": "nrf52840.svd"
|
||||||
"openocd_target": "nrf52840-mdk-rs"
|
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "BQ nRF52840",
|
"name": "BQ nRF52840",
|
||||||
|
|||||||
@@ -33,8 +33,7 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd",
|
"svd_path": "nrf52840.svd"
|
||||||
"openocd_target": "nrf52840-mdk-rs"
|
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "ProMicro compatible nRF52840",
|
"name": "ProMicro compatible nRF52840",
|
||||||
|
|||||||
@@ -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"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -33,8 +33,7 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd",
|
"svd_path": "nrf52840.svd"
|
||||||
"openocd_target": "nrf52840-mdk-rs"
|
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "Seeed T1000-E",
|
"name": "Seeed T1000-E",
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd",
|
"svd_path": "nrf52840.svd"
|
||||||
"openocd_target": "nrf52840-mdk-rs"
|
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino", "freertos"],
|
"frameworks": ["arduino", "freertos"],
|
||||||
"name": "Seeed WIO WM1110",
|
"name": "Seeed WIO WM1110",
|
||||||
|
|||||||
@@ -32,8 +32,7 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd",
|
"svd_path": "nrf52840.svd"
|
||||||
"openocd_target": "nrf52840-mdk-rs"
|
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "Seeed WIO WM1110",
|
"name": "Seeed WIO WM1110",
|
||||||
|
|||||||
@@ -32,8 +32,7 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd",
|
"svd_path": "nrf52840.svd"
|
||||||
"openocd_target": "nrf52840-mdk-rs"
|
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "Seeed WIO WM1110",
|
"name": "Seeed WIO WM1110",
|
||||||
|
|||||||
Vendored
-18
@@ -1,21 +1,3 @@
|
|||||||
meshtasticd (2.7.20.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Version 2.7.20
|
|
||||||
|
|
||||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 11 Feb 2026 12:19:54 +0000
|
|
||||||
|
|
||||||
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
-3
@@ -25,9 +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,
|
|
||||||
libsdl2-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
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
+1
-13
@@ -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)
|
||||||
@@ -49,7 +48,6 @@ BuildRequires: pkgconfig(libulfius)
|
|||||||
BuildRequires: pkgconfig(x11)
|
BuildRequires: pkgconfig(x11)
|
||||||
BuildRequires: pkgconfig(libinput)
|
BuildRequires: pkgconfig(libinput)
|
||||||
BuildRequires: pkgconfig(xkbcommon-x11)
|
BuildRequires: pkgconfig(xkbcommon-x11)
|
||||||
BuildRequires: pkgconfig(sdl2)
|
|
||||||
|
|
||||||
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
|
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
|
||||||
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
|
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
|
||||||
@@ -60,14 +58,8 @@ BuildRequires: pkgconfig(libbsd-overlay)
|
|||||||
|
|
||||||
Requires: systemd-udev
|
Requires: systemd-udev
|
||||||
|
|
||||||
# Declare that this package provides the user/group it creates in %pre
|
|
||||||
# Required for Fedora 43+ which tracks users/groups as RPM dependencies
|
|
||||||
Provides: user(%{meshtasticd_user})
|
|
||||||
Provides: group(%{meshtasticd_user})
|
|
||||||
Provides: group(spi)
|
|
||||||
|
|
||||||
%description
|
%description
|
||||||
Meshtastic daemon. Meshtastic is an off-grid
|
Meshtastic daemon for controlling Meshtastic devices. Meshtastic is an off-grid
|
||||||
text communication platform that uses inexpensive LoRa radios.
|
text communication platform that uses inexpensive LoRa radios.
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
@@ -103,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
|
||||||
@@ -158,7 +147,6 @@ fi
|
|||||||
%license LICENSE
|
%license LICENSE
|
||||||
%doc README.md
|
%doc README.md
|
||||||
%{_bindir}/meshtasticd
|
%{_bindir}/meshtasticd
|
||||||
%{_bindir}/meshtasticd-start.sh
|
|
||||||
%dir %{_localstatedir}/lib/meshtasticd
|
%dir %{_localstatedir}/lib/meshtasticd
|
||||||
%{_udevrulesdir}/99-meshtasticd-udev.rules
|
%{_udevrulesdir}/99-meshtasticd-udev.rules
|
||||||
%dir %{_sysconfdir}/meshtasticd
|
%dir %{_sysconfdir}/meshtasticd
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
# Dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Build output
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Vite cache
|
|
||||||
.vite/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
|
|
||||||
# Local env files
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
# Meshtastic OLED Emulator
|
|
||||||
|
|
||||||
A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays used in Meshtastic devices. This allows firmware UI development and testing directly in the browser.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Same API as firmware**: Uses the same `OLEDDisplay` API as esp8266-oled-ssd1306 library
|
|
||||||
- **Multiple display sizes**: 128x64, 128x32, 64x48, 128x128
|
|
||||||
- **Realistic rendering**: Pixel glow, OLED color tints (blue, white, yellow/blue dual)
|
|
||||||
- **XBM image support**: Render firmware icons and images
|
|
||||||
- **Font rendering**: Bitmap font support with text alignment
|
|
||||||
- **Vue 3 component**: Easy integration with Vue applications
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @meshtastic/oled-emulator
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import {
|
|
||||||
OLEDDisplay,
|
|
||||||
OLEDEmulator,
|
|
||||||
ArialMT_Plain_10,
|
|
||||||
} from "@meshtastic/oled-emulator";
|
|
||||||
|
|
||||||
// Create display matching firmware
|
|
||||||
const display = ref(new OLEDDisplay("128x64"));
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
display.value.setFont(ArialMT_Plain_10);
|
|
||||||
display.value.clear();
|
|
||||||
display.value.drawString(10, 10, "Hello Mesh!");
|
|
||||||
display.value.drawRect(5, 5, 118, 54);
|
|
||||||
display.value.fillCircle(64, 32, 15);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<OLEDEmulator
|
|
||||||
:display="display"
|
|
||||||
:pixel-scale="4"
|
|
||||||
preset="ssd1306-blue"
|
|
||||||
:enable-glow="true"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
## OLEDDisplay API
|
|
||||||
|
|
||||||
The `OLEDDisplay` class provides the same methods as the firmware's display library:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const display = new OLEDDisplay("128x64");
|
|
||||||
|
|
||||||
// Basic drawing
|
|
||||||
display.clear();
|
|
||||||
display.setPixel(x, y);
|
|
||||||
display.drawLine(x0, y0, x1, y1);
|
|
||||||
display.drawRect(x, y, width, height);
|
|
||||||
display.fillRect(x, y, width, height);
|
|
||||||
display.drawCircle(x, y, radius);
|
|
||||||
display.fillCircle(x, y, radius);
|
|
||||||
|
|
||||||
// Text
|
|
||||||
display.setFont(ArialMT_Plain_10);
|
|
||||||
display.setTextAlignment("TEXT_ALIGN_CENTER");
|
|
||||||
display.drawString(x, y, "Hello");
|
|
||||||
display.drawStringMaxWidth(x, y, maxWidth, "Long text...");
|
|
||||||
const width = display.getStringWidth("text");
|
|
||||||
|
|
||||||
// Images (XBM format)
|
|
||||||
display.drawXbm(x, y, width, height, imageData);
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
display.setColor("WHITE"); // or 'BLACK', 'INVERSE'
|
|
||||||
|
|
||||||
// Buffer access (for custom rendering)
|
|
||||||
const buffer = display.getBuffer(); // Uint8Array
|
|
||||||
```
|
|
||||||
|
|
||||||
## Display Presets
|
|
||||||
|
|
||||||
The emulator includes several realistic display presets:
|
|
||||||
|
|
||||||
| Preset | Description |
|
|
||||||
| --------------------- | ------------------------------- |
|
|
||||||
| `ssd1306-blue` | Classic blue OLED (default) |
|
|
||||||
| `ssd1306-white` | White OLED |
|
|
||||||
| `ssd1306-yellow-blue` | Dual-color (top 16 rows yellow) |
|
|
||||||
| `sh1106` | SH1106 style |
|
|
||||||
| `sh1107` | SH1107 128x128 |
|
|
||||||
| `ssd1306-64x48` | Small 64x48 display |
|
|
||||||
| `ssd1306-128x32` | Wide 128x32 display |
|
|
||||||
|
|
||||||
## Screen Renderers
|
|
||||||
|
|
||||||
Pre-built screen renderers matching firmware UI:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { ScreenRenderers } from "@meshtastic/oled-emulator";
|
|
||||||
|
|
||||||
// Boot screen
|
|
||||||
ScreenRenderers.drawBootScreen(display, "2.5.0");
|
|
||||||
|
|
||||||
// Node info screen
|
|
||||||
ScreenRenderers.drawNodeInfoScreen(display, {
|
|
||||||
shortName: "MESH",
|
|
||||||
longName: "My Node",
|
|
||||||
nodeId: "!abcd1234",
|
|
||||||
lastHeard: "2m ago",
|
|
||||||
snr: 12.5,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Message screen
|
|
||||||
ScreenRenderers.drawMessageScreen(display, {
|
|
||||||
from: "Alice",
|
|
||||||
text: "Hello from the mesh!",
|
|
||||||
time: "14:32",
|
|
||||||
channel: "LongFast",
|
|
||||||
});
|
|
||||||
|
|
||||||
// And more: drawGPSScreen, drawNodeListScreen, drawSystemScreen, drawCompassScreen
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Run development server
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# Build library
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future: Protocol Bridge
|
|
||||||
|
|
||||||
A future enhancement will allow connecting this emulator to a real Meshtastic firmware instance (running on native/portduino), enabling live framebuffer streaming over WebSocket for real-time display mirroring.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
GPL-3.0 - Same as Meshtastic firmware
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Meshtastic OLED Emulator</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
background: #121212;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Generated
-2211
File diff suppressed because it is too large
Load Diff
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@meshtastic/oled-emulator",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Vue.js OLED display emulator for Meshtastic firmware UI development",
|
|
||||||
"keywords": [
|
|
||||||
"meshtastic",
|
|
||||||
"oled",
|
|
||||||
"ssd1306",
|
|
||||||
"sh1106",
|
|
||||||
"display",
|
|
||||||
"emulator",
|
|
||||||
"vue",
|
|
||||||
"vue3"
|
|
||||||
],
|
|
||||||
"author": "Meshtastic",
|
|
||||||
"license": "GPL-3.0",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/meshtastic/firmware.git",
|
|
||||||
"directory": "oled-emulator"
|
|
||||||
},
|
|
||||||
"main": "./dist/index.js",
|
|
||||||
"module": "./dist/index.mjs",
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"import": "./dist/index.mjs",
|
|
||||||
"require": "./dist/index.js",
|
|
||||||
"types": "./dist/index.d.ts"
|
|
||||||
},
|
|
||||||
"./style.css": "./dist/style.css"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vue-tsc && vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"type-check": "vue-tsc --noEmit"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.3.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
|
||||||
"typescript": "^5.3.0",
|
|
||||||
"vite": "^5.0.0",
|
|
||||||
"vite-plugin-dts": "^3.7.0",
|
|
||||||
"vue": "^3.4.0",
|
|
||||||
"vue-tsc": "^2.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,82 +0,0 @@
|
|||||||
/**
|
|
||||||
* DisplayPresets.ts - OLED display presets for emulator
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { OLEDDISPLAY_GEOMETRY } from "./OLEDDisplay";
|
|
||||||
|
|
||||||
// Display type presets with realistic OLED colors
|
|
||||||
export interface DisplayPreset {
|
|
||||||
name: string;
|
|
||||||
geometry: OLEDDISPLAY_GEOMETRY;
|
|
||||||
pixelOn: { r: number; g: number; b: number };
|
|
||||||
pixelOff: { r: number; g: number; b: number };
|
|
||||||
glow: number;
|
|
||||||
contrast: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DISPLAY_PRESETS: Record<string, DisplayPreset> = {
|
|
||||||
// Classic blue OLED (SSD1306)
|
|
||||||
"ssd1306-blue": {
|
|
||||||
name: "SSD1306 Blue",
|
|
||||||
geometry: "128x64",
|
|
||||||
pixelOn: { r: 100, g: 180, b: 255 },
|
|
||||||
pixelOff: { r: 2, g: 3, b: 8 },
|
|
||||||
glow: 0.4,
|
|
||||||
contrast: 1.0,
|
|
||||||
},
|
|
||||||
// White OLED (SSD1306)
|
|
||||||
"ssd1306-white": {
|
|
||||||
name: "SSD1306 White",
|
|
||||||
geometry: "128x64",
|
|
||||||
pixelOn: { r: 255, g: 255, b: 245 },
|
|
||||||
pixelOff: { r: 5, g: 5, b: 5 },
|
|
||||||
glow: 0.3,
|
|
||||||
contrast: 1.0,
|
|
||||||
},
|
|
||||||
// Yellow-blue dual color OLED
|
|
||||||
"ssd1306-yellow-blue": {
|
|
||||||
name: "SSD1306 Yellow/Blue",
|
|
||||||
geometry: "128x64",
|
|
||||||
// Yellow section is top 16 rows, blue is rest
|
|
||||||
pixelOn: { r: 255, g: 220, b: 50 }, // Will be overridden per-pixel
|
|
||||||
pixelOff: { r: 3, g: 3, b: 5 },
|
|
||||||
glow: 0.35,
|
|
||||||
contrast: 1.0,
|
|
||||||
},
|
|
||||||
// SH1106 (slightly different characteristics)
|
|
||||||
sh1106: {
|
|
||||||
name: "SH1106",
|
|
||||||
geometry: "128x64",
|
|
||||||
pixelOn: { r: 120, g: 200, b: 255 },
|
|
||||||
pixelOff: { r: 2, g: 2, b: 6 },
|
|
||||||
glow: 0.45,
|
|
||||||
contrast: 0.95,
|
|
||||||
},
|
|
||||||
// SH1107 128x128
|
|
||||||
sh1107: {
|
|
||||||
name: "SH1107 128x128",
|
|
||||||
geometry: "128x128",
|
|
||||||
pixelOn: { r: 255, g: 255, b: 240 },
|
|
||||||
pixelOff: { r: 4, g: 4, b: 6 },
|
|
||||||
glow: 0.3,
|
|
||||||
contrast: 1.0,
|
|
||||||
},
|
|
||||||
// Small 64x48 OLED
|
|
||||||
"ssd1306-64x48": {
|
|
||||||
name: "SSD1306 64x48",
|
|
||||||
geometry: "64x48",
|
|
||||||
pixelOn: { r: 100, g: 180, b: 255 },
|
|
||||||
pixelOff: { r: 2, g: 3, b: 8 },
|
|
||||||
glow: 0.4,
|
|
||||||
contrast: 1.0,
|
|
||||||
},
|
|
||||||
// Mini 128x32
|
|
||||||
"ssd1306-128x32": {
|
|
||||||
name: "SSD1306 128x32",
|
|
||||||
geometry: "128x32",
|
|
||||||
pixelOn: { r: 100, g: 180, b: 255 },
|
|
||||||
pixelOff: { r: 2, g: 3, b: 8 },
|
|
||||||
glow: 0.4,
|
|
||||||
contrast: 1.0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,557 +0,0 @@
|
|||||||
/**
|
|
||||||
* OLEDDisplay.ts - TypeScript port of esp8266-oled-ssd1306 OLEDDisplay API
|
|
||||||
*
|
|
||||||
* This provides the same drawing API as the firmware uses, allowing
|
|
||||||
* Meshtastic screen rendering code to be ported to the web.
|
|
||||||
*
|
|
||||||
* Buffer format: Vertical byte packing (8 vertical pixels per byte)
|
|
||||||
* Same as SSD1306/SH1106 native format.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type OLEDDISPLAY_GEOMETRY =
|
|
||||||
| "128x64"
|
|
||||||
| "128x32"
|
|
||||||
| "64x48"
|
|
||||||
| "64x32"
|
|
||||||
| "128x128"
|
|
||||||
| "96x16";
|
|
||||||
|
|
||||||
export type OLEDDISPLAY_COLOR = "WHITE" | "BLACK" | "INVERSE";
|
|
||||||
|
|
||||||
export type OLEDDISPLAY_TEXT_ALIGNMENT =
|
|
||||||
| "TEXT_ALIGN_LEFT"
|
|
||||||
| "TEXT_ALIGN_CENTER"
|
|
||||||
| "TEXT_ALIGN_RIGHT";
|
|
||||||
|
|
||||||
export interface OLEDFont {
|
|
||||||
height: number;
|
|
||||||
firstChar: number;
|
|
||||||
charCount: number;
|
|
||||||
widths: number[];
|
|
||||||
data: Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolution info based on geometry
|
|
||||||
const GEOMETRY_SIZES: Record<
|
|
||||||
OLEDDISPLAY_GEOMETRY,
|
|
||||||
{ width: number; height: number }
|
|
||||||
> = {
|
|
||||||
"128x64": { width: 128, height: 64 },
|
|
||||||
"128x32": { width: 128, height: 32 },
|
|
||||||
"64x48": { width: 64, height: 48 },
|
|
||||||
"64x32": { width: 64, height: 32 },
|
|
||||||
"128x128": { width: 128, height: 128 },
|
|
||||||
"96x16": { width: 96, height: 16 },
|
|
||||||
};
|
|
||||||
|
|
||||||
export class OLEDDisplay {
|
|
||||||
private buffer: Uint8Array;
|
|
||||||
private _width: number;
|
|
||||||
private _height: number;
|
|
||||||
private color: OLEDDISPLAY_COLOR = "WHITE";
|
|
||||||
private textAlignment: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
|
|
||||||
private font: OLEDFont | null = null;
|
|
||||||
|
|
||||||
constructor(geometry: OLEDDISPLAY_GEOMETRY = "128x64") {
|
|
||||||
const size = GEOMETRY_SIZES[geometry];
|
|
||||||
this._width = size.width;
|
|
||||||
this._height = size.height;
|
|
||||||
// Buffer size: width * (height / 8) - vertical byte packing
|
|
||||||
this.buffer = new Uint8Array((this._width * this._height) / 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
get width(): number {
|
|
||||||
return this._width;
|
|
||||||
}
|
|
||||||
|
|
||||||
get height(): number {
|
|
||||||
return this._height;
|
|
||||||
}
|
|
||||||
|
|
||||||
getWidth(): number {
|
|
||||||
return this._width;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHeight(): number {
|
|
||||||
return this._height;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the raw framebuffer (vertical byte packing format)
|
|
||||||
*/
|
|
||||||
getBuffer(): Uint8Array {
|
|
||||||
return this.buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBufferSize(): number {
|
|
||||||
return this.buffer.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the display buffer
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
this.buffer.fill(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the drawing color
|
|
||||||
*/
|
|
||||||
setColor(color: OLEDDISPLAY_COLOR): void {
|
|
||||||
this.color = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set text alignment for drawString
|
|
||||||
*/
|
|
||||||
setTextAlignment(align: OLEDDISPLAY_TEXT_ALIGNMENT): void {
|
|
||||||
this.textAlignment = align;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current font
|
|
||||||
*/
|
|
||||||
setFont(font: OLEDFont): void {
|
|
||||||
this.font = font;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a single pixel
|
|
||||||
*/
|
|
||||||
setPixel(x: number, y: number): void {
|
|
||||||
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
|
|
||||||
|
|
||||||
// Vertical byte packing: each byte represents 8 vertical pixels
|
|
||||||
const byteIndex = x + Math.floor(y / 8) * this._width;
|
|
||||||
const bitMask = 1 << (y & 7);
|
|
||||||
|
|
||||||
switch (this.color) {
|
|
||||||
case "WHITE":
|
|
||||||
this.buffer[byteIndex] |= bitMask;
|
|
||||||
break;
|
|
||||||
case "BLACK":
|
|
||||||
this.buffer[byteIndex] &= ~bitMask;
|
|
||||||
break;
|
|
||||||
case "INVERSE":
|
|
||||||
this.buffer[byteIndex] ^= bitMask;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear a single pixel (set to black)
|
|
||||||
*/
|
|
||||||
clearPixel(x: number, y: number): void {
|
|
||||||
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
|
|
||||||
const byteIndex = x + Math.floor(y / 8) * this._width;
|
|
||||||
const bitMask = 1 << (y & 7);
|
|
||||||
this.buffer[byteIndex] &= ~bitMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a pixel value (true = on, false = off)
|
|
||||||
*/
|
|
||||||
getPixel(x: number, y: number): boolean {
|
|
||||||
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return false;
|
|
||||||
const byteIndex = x + Math.floor(y / 8) * this._width;
|
|
||||||
const bitMask = 1 << (y & 7);
|
|
||||||
return (this.buffer[byteIndex] & bitMask) !== 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a line using Bresenham's algorithm
|
|
||||||
*/
|
|
||||||
drawLine(x0: number, y0: number, x1: number, y1: number): void {
|
|
||||||
const dx = Math.abs(x1 - x0);
|
|
||||||
const dy = Math.abs(y1 - y0);
|
|
||||||
const sx = x0 < x1 ? 1 : -1;
|
|
||||||
const sy = y0 < y1 ? 1 : -1;
|
|
||||||
let err = dx - dy;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
this.setPixel(x0, y0);
|
|
||||||
if (x0 === x1 && y0 === y1) break;
|
|
||||||
const e2 = 2 * err;
|
|
||||||
if (e2 > -dy) {
|
|
||||||
err -= dy;
|
|
||||||
x0 += sx;
|
|
||||||
}
|
|
||||||
if (e2 < dx) {
|
|
||||||
err += dx;
|
|
||||||
y0 += sy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw horizontal line (optimized)
|
|
||||||
*/
|
|
||||||
drawHorizontalLine(x: number, y: number, length: number): void {
|
|
||||||
if (y < 0 || y >= this._height) return;
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
this.setPixel(x + i, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw vertical line (optimized)
|
|
||||||
*/
|
|
||||||
drawVerticalLine(x: number, y: number, length: number): void {
|
|
||||||
if (x < 0 || x >= this._width) return;
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
this.setPixel(x, y + i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a rectangle outline
|
|
||||||
*/
|
|
||||||
drawRect(x: number, y: number, width: number, height: number): void {
|
|
||||||
this.drawHorizontalLine(x, y, width);
|
|
||||||
this.drawHorizontalLine(x, y + height - 1, width);
|
|
||||||
this.drawVerticalLine(x, y, height);
|
|
||||||
this.drawVerticalLine(x + width - 1, y, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a filled rectangle
|
|
||||||
*/
|
|
||||||
fillRect(x: number, y: number, width: number, height: number): void {
|
|
||||||
for (let i = 0; i < width; i++) {
|
|
||||||
for (let j = 0; j < height; j++) {
|
|
||||||
this.setPixel(x + i, y + j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a circle outline using midpoint algorithm
|
|
||||||
*/
|
|
||||||
drawCircle(x0: number, y0: number, radius: number): void {
|
|
||||||
let x = radius;
|
|
||||||
let y = 0;
|
|
||||||
let err = 0;
|
|
||||||
|
|
||||||
while (x >= y) {
|
|
||||||
this.setPixel(x0 + x, y0 + y);
|
|
||||||
this.setPixel(x0 + y, y0 + x);
|
|
||||||
this.setPixel(x0 - y, y0 + x);
|
|
||||||
this.setPixel(x0 - x, y0 + y);
|
|
||||||
this.setPixel(x0 - x, y0 - y);
|
|
||||||
this.setPixel(x0 - y, y0 - x);
|
|
||||||
this.setPixel(x0 + y, y0 - x);
|
|
||||||
this.setPixel(x0 + x, y0 - y);
|
|
||||||
|
|
||||||
y++;
|
|
||||||
if (err <= 0) {
|
|
||||||
err += 2 * y + 1;
|
|
||||||
}
|
|
||||||
if (err > 0) {
|
|
||||||
x--;
|
|
||||||
err -= 2 * x + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a filled circle
|
|
||||||
*/
|
|
||||||
fillCircle(x0: number, y0: number, radius: number): void {
|
|
||||||
let x = radius;
|
|
||||||
let y = 0;
|
|
||||||
let err = 0;
|
|
||||||
|
|
||||||
while (x >= y) {
|
|
||||||
this.drawHorizontalLine(x0 - x, y0 + y, 2 * x + 1);
|
|
||||||
this.drawHorizontalLine(x0 - y, y0 + x, 2 * y + 1);
|
|
||||||
this.drawHorizontalLine(x0 - x, y0 - y, 2 * x + 1);
|
|
||||||
this.drawHorizontalLine(x0 - y, y0 - x, 2 * y + 1);
|
|
||||||
|
|
||||||
y++;
|
|
||||||
if (err <= 0) {
|
|
||||||
err += 2 * y + 1;
|
|
||||||
}
|
|
||||||
if (err > 0) {
|
|
||||||
x--;
|
|
||||||
err -= 2 * x + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a quarter circle (quadrant 0-3)
|
|
||||||
*/
|
|
||||||
drawCircleQuads(x0: number, y0: number, radius: number, quads: number): void {
|
|
||||||
let x = radius;
|
|
||||||
let y = 0;
|
|
||||||
let err = 0;
|
|
||||||
|
|
||||||
while (x >= y) {
|
|
||||||
if (quads & 0x1) {
|
|
||||||
this.setPixel(x0 + x, y0 - y);
|
|
||||||
this.setPixel(x0 + y, y0 - x);
|
|
||||||
}
|
|
||||||
if (quads & 0x2) {
|
|
||||||
this.setPixel(x0 - y, y0 - x);
|
|
||||||
this.setPixel(x0 - x, y0 - y);
|
|
||||||
}
|
|
||||||
if (quads & 0x4) {
|
|
||||||
this.setPixel(x0 - x, y0 + y);
|
|
||||||
this.setPixel(x0 - y, y0 + x);
|
|
||||||
}
|
|
||||||
if (quads & 0x8) {
|
|
||||||
this.setPixel(x0 + y, y0 + x);
|
|
||||||
this.setPixel(x0 + x, y0 + y);
|
|
||||||
}
|
|
||||||
|
|
||||||
y++;
|
|
||||||
if (err <= 0) {
|
|
||||||
err += 2 * y + 1;
|
|
||||||
}
|
|
||||||
if (err > 0) {
|
|
||||||
x--;
|
|
||||||
err -= 2 * x + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw an XBM image (like firmware's drawXbm)
|
|
||||||
* XBM format: LSB first, horizontal
|
|
||||||
*/
|
|
||||||
drawXbm(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
xbm: Uint8Array | number[],
|
|
||||||
): void {
|
|
||||||
const widthBytes = Math.ceil(width / 8);
|
|
||||||
for (let j = 0; j < height; j++) {
|
|
||||||
for (let i = 0; i < width; i++) {
|
|
||||||
const byteIndex = j * widthBytes + Math.floor(i / 8);
|
|
||||||
const bitIndex = i % 8;
|
|
||||||
if (xbm[byteIndex] & (1 << bitIndex)) {
|
|
||||||
this.setPixel(x + i, y + j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a PROGMEM-style image (vertical byte format, like some firmware icons)
|
|
||||||
*/
|
|
||||||
drawFastImage(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
image: Uint8Array | number[],
|
|
||||||
): void {
|
|
||||||
const heightInBytes = Math.ceil(height / 8);
|
|
||||||
for (let i = 0; i < width; i++) {
|
|
||||||
for (let j = 0; j < heightInBytes; j++) {
|
|
||||||
const imageByte = image[i + j * width];
|
|
||||||
for (let bit = 0; bit < 8; bit++) {
|
|
||||||
if (imageByte & (1 << bit)) {
|
|
||||||
this.setPixel(x + i, y + j * 8 + bit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the width of a string with the current font
|
|
||||||
*/
|
|
||||||
getStringWidth(text: string): number {
|
|
||||||
if (!this.font) return 0;
|
|
||||||
let width = 0;
|
|
||||||
for (const char of text) {
|
|
||||||
const charCode = char.charCodeAt(0);
|
|
||||||
if (
|
|
||||||
charCode >= this.font.firstChar &&
|
|
||||||
charCode < this.font.firstChar + this.font.charCount
|
|
||||||
) {
|
|
||||||
width += this.font.widths[charCode - this.font.firstChar];
|
|
||||||
} else {
|
|
||||||
// Unknown char - use space width or default
|
|
||||||
width += this.font.widths[0] || 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return width;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a string at the given position
|
|
||||||
*/
|
|
||||||
drawString(x: number, y: number, text: string): void {
|
|
||||||
if (!this.font) return;
|
|
||||||
|
|
||||||
// Adjust x based on alignment
|
|
||||||
let startX = x;
|
|
||||||
if (this.textAlignment === "TEXT_ALIGN_CENTER") {
|
|
||||||
startX = x - Math.floor(this.getStringWidth(text) / 2);
|
|
||||||
} else if (this.textAlignment === "TEXT_ALIGN_RIGHT") {
|
|
||||||
startX = x - this.getStringWidth(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cursorX = startX;
|
|
||||||
for (const char of text) {
|
|
||||||
const charCode = char.charCodeAt(0);
|
|
||||||
if (
|
|
||||||
charCode >= this.font.firstChar &&
|
|
||||||
charCode < this.font.firstChar + this.font.charCount
|
|
||||||
) {
|
|
||||||
const charIndex = charCode - this.font.firstChar;
|
|
||||||
const charWidth = this.font.widths[charIndex];
|
|
||||||
|
|
||||||
// Find the offset into the font data
|
|
||||||
let offset = 0;
|
|
||||||
const bytesPerColumn = Math.ceil(this.font.height / 8);
|
|
||||||
for (let i = 0; i < charIndex; i++) {
|
|
||||||
offset += this.font.widths[i] * bytesPerColumn;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the character
|
|
||||||
this.drawFontChar(
|
|
||||||
cursorX,
|
|
||||||
y,
|
|
||||||
charWidth,
|
|
||||||
this.font.height,
|
|
||||||
this.font.data,
|
|
||||||
offset,
|
|
||||||
);
|
|
||||||
cursorX += charWidth;
|
|
||||||
} else {
|
|
||||||
// Unknown character - advance by space
|
|
||||||
cursorX += this.font.widths[0] || 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a single font character
|
|
||||||
*/
|
|
||||||
private drawFontChar(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
data: Uint8Array,
|
|
||||||
offset: number,
|
|
||||||
): void {
|
|
||||||
const bytesPerColumn = Math.ceil(height / 8);
|
|
||||||
for (let col = 0; col < width; col++) {
|
|
||||||
for (let byteRow = 0; byteRow < bytesPerColumn; byteRow++) {
|
|
||||||
const dataByte = data[offset + col * bytesPerColumn + byteRow];
|
|
||||||
for (let bit = 0; bit < 8; bit++) {
|
|
||||||
const pixelY = byteRow * 8 + bit;
|
|
||||||
if (pixelY < height && dataByte & (1 << bit)) {
|
|
||||||
this.setPixel(x + col, y + pixelY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a string that wraps within maxWidth
|
|
||||||
*/
|
|
||||||
drawStringMaxWidth(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
maxWidth: number,
|
|
||||||
text: string,
|
|
||||||
): void {
|
|
||||||
if (!this.font) return;
|
|
||||||
|
|
||||||
const words = text.split(" ");
|
|
||||||
let line = "";
|
|
||||||
let lineY = y;
|
|
||||||
|
|
||||||
for (const word of words) {
|
|
||||||
const testLine = line + (line ? " " : "") + word;
|
|
||||||
const testWidth = this.getStringWidth(testLine);
|
|
||||||
|
|
||||||
if (testWidth > maxWidth && line) {
|
|
||||||
this.drawString(x, lineY, line);
|
|
||||||
line = word;
|
|
||||||
lineY += this.font.height;
|
|
||||||
} else {
|
|
||||||
line = testLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line) {
|
|
||||||
this.drawString(x, lineY, line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a progress bar
|
|
||||||
*/
|
|
||||||
drawProgressBar(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
progress: number,
|
|
||||||
): void {
|
|
||||||
// Clamp progress 0-100
|
|
||||||
progress = Math.max(0, Math.min(100, progress));
|
|
||||||
|
|
||||||
// Draw outline
|
|
||||||
this.drawRect(x, y, width, height);
|
|
||||||
|
|
||||||
// Draw fill
|
|
||||||
const fillWidth = Math.floor(((width - 4) * progress) / 100);
|
|
||||||
this.fillRect(x + 2, y + 2, fillWidth, height - 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display - no-op for emulator (would send buffer to hardware)
|
|
||||||
*/
|
|
||||||
display(): void {
|
|
||||||
// In real hardware this sends the buffer to the display
|
|
||||||
// For emulator, this is a no-op - we render from getBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flip the display (useful for some mounting orientations)
|
|
||||||
*/
|
|
||||||
flipScreenVertically(): void {
|
|
||||||
// Rotate buffer 180 degrees
|
|
||||||
const newBuffer = new Uint8Array(this.buffer.length);
|
|
||||||
const heightBytes = this._height / 8;
|
|
||||||
|
|
||||||
for (let x = 0; x < this._width; x++) {
|
|
||||||
for (let yByte = 0; yByte < heightBytes; yByte++) {
|
|
||||||
let srcByte = this.buffer[x + yByte * this._width];
|
|
||||||
// Reverse the bits
|
|
||||||
let reversed = 0;
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
if (srcByte & (1 << i)) {
|
|
||||||
reversed |= 1 << (7 - i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Place in mirrored position
|
|
||||||
const newX = this._width - 1 - x;
|
|
||||||
const newYByte = heightBytes - 1 - yByte;
|
|
||||||
newBuffer[newX + newYByte * this._width] = reversed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.buffer = newBuffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Color constants for compatibility with firmware code style
|
|
||||||
export const WHITE: OLEDDISPLAY_COLOR = "WHITE";
|
|
||||||
export const BLACK: OLEDDISPLAY_COLOR = "BLACK";
|
|
||||||
export const INVERSE: OLEDDISPLAY_COLOR = "INVERSE";
|
|
||||||
|
|
||||||
export const TEXT_ALIGN_LEFT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
|
|
||||||
export const TEXT_ALIGN_CENTER: OLEDDISPLAY_TEXT_ALIGNMENT =
|
|
||||||
"TEXT_ALIGN_CENTER";
|
|
||||||
export const TEXT_ALIGN_RIGHT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_RIGHT";
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
/**
|
|
||||||
* OLEDEmulator.vue - Vue 3 component for OLED display emulation
|
|
||||||
*
|
|
||||||
* Renders an OLEDDisplay framebuffer with realistic OLED visual effects
|
|
||||||
* including pixel glow, color tinting, and various display sizes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ref, computed, watch, onMounted, onUnmounted, type PropType } from 'vue';
|
|
||||||
import { OLEDDisplay } from './OLEDDisplay';
|
|
||||||
import { DISPLAY_PRESETS, type DisplayPreset } from './DisplayPresets';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
/**
|
|
||||||
* The OLEDDisplay instance to render
|
|
||||||
*/
|
|
||||||
display: {
|
|
||||||
type: Object as PropType<OLEDDisplay>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Pixel scale factor (how many canvas pixels per OLED pixel)
|
|
||||||
*/
|
|
||||||
pixelScale: {
|
|
||||||
type: Number,
|
|
||||||
default: 4,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Display preset name or custom preset object
|
|
||||||
*/
|
|
||||||
preset: {
|
|
||||||
type: [String, Object] as PropType<string | DisplayPreset>,
|
|
||||||
default: 'ssd1306-blue',
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Enable pixel glow effect (slight bloom around lit pixels)
|
|
||||||
*/
|
|
||||||
enableGlow: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Enable sub-pixel rendering for more realistic look
|
|
||||||
*/
|
|
||||||
enableSubPixel: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Show pixel grid lines
|
|
||||||
*/
|
|
||||||
showGrid: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Auto-refresh interval in ms (0 = manual refresh only)
|
|
||||||
*/
|
|
||||||
refreshInterval: {
|
|
||||||
type: Number,
|
|
||||||
default: 50,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Yellow/blue split mode for dual-color OLEDs (rows 0-15 yellow)
|
|
||||||
*/
|
|
||||||
dualColor: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'click', x: number, y: number): void;
|
|
||||||
(e: 'refresh'): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const canvas = ref<HTMLCanvasElement | null>(null);
|
|
||||||
const refreshTimer = ref<number | null>(null);
|
|
||||||
|
|
||||||
// Get the current display preset
|
|
||||||
const currentPreset = computed<DisplayPreset>(() => {
|
|
||||||
if (typeof props.preset === 'string') {
|
|
||||||
return DISPLAY_PRESETS[props.preset] || DISPLAY_PRESETS['ssd1306-blue'];
|
|
||||||
}
|
|
||||||
return props.preset;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Canvas dimensions
|
|
||||||
const canvasWidth = computed(() => props.display.width * props.pixelScale);
|
|
||||||
const canvasHeight = computed(() => props.display.height * props.pixelScale);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the framebuffer to canvas with OLED effects
|
|
||||||
*/
|
|
||||||
function render(): void {
|
|
||||||
const cvs = canvas.value;
|
|
||||||
if (!cvs) return;
|
|
||||||
|
|
||||||
const ctx = cvs.getContext('2d');
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
const buffer = props.display.getBuffer();
|
|
||||||
const width = props.display.width;
|
|
||||||
const height = props.display.height;
|
|
||||||
const scale = props.pixelScale;
|
|
||||||
const preset = currentPreset.value;
|
|
||||||
|
|
||||||
// Create image data
|
|
||||||
const imageData = ctx.createImageData(canvasWidth.value, canvasHeight.value);
|
|
||||||
const data = imageData.data;
|
|
||||||
|
|
||||||
// Yellow color for dual-color mode (top 16 rows)
|
|
||||||
const yellowOn = { r: 255, g: 220, b: 50 };
|
|
||||||
const blueOn = { r: 100, g: 180, b: 255 };
|
|
||||||
|
|
||||||
// Render each OLED pixel
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
for (let x = 0; x < width; x++) {
|
|
||||||
// Get pixel from framebuffer (vertical byte packing)
|
|
||||||
const byteIndex = x + Math.floor(y / 8) * width;
|
|
||||||
const bitMask = 1 << (y & 7);
|
|
||||||
const isOn = (buffer[byteIndex] & bitMask) !== 0;
|
|
||||||
|
|
||||||
// Determine pixel color
|
|
||||||
let pixelColor: { r: number; g: number; b: number };
|
|
||||||
if (isOn) {
|
|
||||||
if (props.dualColor && y < 16) {
|
|
||||||
pixelColor = yellowOn;
|
|
||||||
} else if (props.dualColor) {
|
|
||||||
pixelColor = blueOn;
|
|
||||||
} else {
|
|
||||||
pixelColor = preset.pixelOn;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pixelColor = preset.pixelOff;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply contrast
|
|
||||||
pixelColor = {
|
|
||||||
r: Math.min(255, pixelColor.r * preset.contrast),
|
|
||||||
g: Math.min(255, pixelColor.g * preset.contrast),
|
|
||||||
b: Math.min(255, pixelColor.b * preset.contrast),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Scale up pixel to canvas
|
|
||||||
for (let sy = 0; sy < scale; sy++) {
|
|
||||||
for (let sx = 0; sx < scale; sx++) {
|
|
||||||
const px = x * scale + sx;
|
|
||||||
const py = y * scale + sy;
|
|
||||||
const idx = (py * canvasWidth.value + px) * 4;
|
|
||||||
|
|
||||||
// Apply glow effect (brighter center, dimmer edges)
|
|
||||||
let intensity = 1.0;
|
|
||||||
if (props.enableGlow && isOn) {
|
|
||||||
const centerX = scale / 2;
|
|
||||||
const centerY = scale / 2;
|
|
||||||
const dist = Math.sqrt(
|
|
||||||
Math.pow(sx - centerX, 2) + Math.pow(sy - centerY, 2)
|
|
||||||
);
|
|
||||||
const maxDist = Math.sqrt(2) * (scale / 2);
|
|
||||||
intensity = 1.0 - (dist / maxDist) * preset.glow;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply sub-pixel rendering (RGB stripes)
|
|
||||||
if (props.enableSubPixel && isOn) {
|
|
||||||
const subPixelPos = sx % 3;
|
|
||||||
if (subPixelPos === 0) {
|
|
||||||
data[idx] = pixelColor.r * intensity * 1.2;
|
|
||||||
data[idx + 1] = pixelColor.g * intensity * 0.8;
|
|
||||||
data[idx + 2] = pixelColor.b * intensity * 0.8;
|
|
||||||
} else if (subPixelPos === 1) {
|
|
||||||
data[idx] = pixelColor.r * intensity * 0.8;
|
|
||||||
data[idx + 1] = pixelColor.g * intensity * 1.2;
|
|
||||||
data[idx + 2] = pixelColor.b * intensity * 0.8;
|
|
||||||
} else {
|
|
||||||
data[idx] = pixelColor.r * intensity * 0.8;
|
|
||||||
data[idx + 1] = pixelColor.g * intensity * 0.8;
|
|
||||||
data[idx + 2] = pixelColor.b * intensity * 1.2;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data[idx] = pixelColor.r * intensity;
|
|
||||||
data[idx + 1] = pixelColor.g * intensity;
|
|
||||||
data[idx + 2] = pixelColor.b * intensity;
|
|
||||||
}
|
|
||||||
data[idx + 3] = 255; // Alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw grid lines if enabled
|
|
||||||
if (props.showGrid) {
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
for (let x = 0; x < width; x++) {
|
|
||||||
// Draw right and bottom edge of each pixel
|
|
||||||
// Right edge
|
|
||||||
const rx = (x + 1) * scale - 1;
|
|
||||||
for (let sy = 0; sy < scale; sy++) {
|
|
||||||
const py = y * scale + sy;
|
|
||||||
const idx = (py * canvasWidth.value + rx) * 4;
|
|
||||||
data[idx] = Math.min(255, data[idx] + 20);
|
|
||||||
data[idx + 1] = Math.min(255, data[idx + 1] + 20);
|
|
||||||
data[idx + 2] = Math.min(255, data[idx + 2] + 20);
|
|
||||||
}
|
|
||||||
// Bottom edge
|
|
||||||
const by = (y + 1) * scale - 1;
|
|
||||||
for (let sx = 0; sx < scale; sx++) {
|
|
||||||
const px = x * scale + sx;
|
|
||||||
const idx = (by * canvasWidth.value + px) * 4;
|
|
||||||
data[idx] = Math.min(255, data[idx] + 20);
|
|
||||||
data[idx + 1] = Math.min(255, data[idx + 1] + 20);
|
|
||||||
data[idx + 2] = Math.min(255, data[idx + 2] + 20);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.putImageData(imageData, 0, 0);
|
|
||||||
emit('refresh');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle canvas click
|
|
||||||
*/
|
|
||||||
function handleClick(event: MouseEvent): void {
|
|
||||||
const cvs = canvas.value;
|
|
||||||
if (!cvs) return;
|
|
||||||
|
|
||||||
const rect = cvs.getBoundingClientRect();
|
|
||||||
const scaleX = cvs.width / rect.width;
|
|
||||||
const scaleY = cvs.height / rect.height;
|
|
||||||
|
|
||||||
const canvasX = (event.clientX - rect.left) * scaleX;
|
|
||||||
const canvasY = (event.clientY - rect.top) * scaleY;
|
|
||||||
|
|
||||||
const oledX = Math.floor(canvasX / props.pixelScale);
|
|
||||||
const oledY = Math.floor(canvasY / props.pixelScale);
|
|
||||||
|
|
||||||
emit('click', oledX, oledY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start auto-refresh timer
|
|
||||||
*/
|
|
||||||
function startRefreshTimer(): void {
|
|
||||||
stopRefreshTimer();
|
|
||||||
if (props.refreshInterval > 0) {
|
|
||||||
refreshTimer.value = window.setInterval(render, props.refreshInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop auto-refresh timer
|
|
||||||
*/
|
|
||||||
function stopRefreshTimer(): void {
|
|
||||||
if (refreshTimer.value !== null) {
|
|
||||||
clearInterval(refreshTimer.value);
|
|
||||||
refreshTimer.value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
onMounted(() => {
|
|
||||||
render();
|
|
||||||
startRefreshTimer();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopRefreshTimer();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Watch for changes
|
|
||||||
watch(() => props.refreshInterval, startRefreshTimer);
|
|
||||||
watch(() => props.display, render, { deep: true });
|
|
||||||
watch(() => props.pixelScale, render);
|
|
||||||
watch(() => props.preset, render);
|
|
||||||
watch(() => props.enableGlow, render);
|
|
||||||
watch(() => props.showGrid, render);
|
|
||||||
watch(() => props.dualColor, render);
|
|
||||||
|
|
||||||
// Expose render method for manual refresh
|
|
||||||
defineExpose({ render });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="oled-emulator-wrapper">
|
|
||||||
<canvas
|
|
||||||
ref="canvas"
|
|
||||||
:width="canvasWidth"
|
|
||||||
:height="canvasHeight"
|
|
||||||
class="oled-canvas"
|
|
||||||
@click="handleClick"
|
|
||||||
/>
|
|
||||||
<div class="oled-bezel" v-if="$slots.bezel">
|
|
||||||
<slot name="bezel" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.oled-emulator-wrapper {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
background: #1a1a1a;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow:
|
|
||||||
0 2px 8px rgba(0, 0, 0, 0.4),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.oled-canvas {
|
|
||||||
display: block;
|
|
||||||
border-radius: 2px;
|
|
||||||
image-rendering: pixelated;
|
|
||||||
image-rendering: crisp-edges;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oled-bezel {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
/**
|
|
||||||
* OLEDFonts.ts - Font data for OLED display emulator
|
|
||||||
*
|
|
||||||
* These are simplified bitmap fonts matching the style used in
|
|
||||||
* Meshtastic firmware (based on ArialMT from esp8266-oled-ssd1306).
|
|
||||||
*
|
|
||||||
* Font format: Vertical byte packing, column-major order
|
|
||||||
* Each character is drawn column by column, with each byte representing
|
|
||||||
* 8 vertical pixels.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { OLEDFont } from "./OLEDDisplay";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ArialMT Plain 10 - Small font (height ~13 pixels)
|
|
||||||
* Covers ASCII 32-126 (space through ~)
|
|
||||||
*/
|
|
||||||
export const ArialMT_Plain_10: OLEDFont = {
|
|
||||||
height: 13,
|
|
||||||
firstChar: 32,
|
|
||||||
charCount: 95,
|
|
||||||
widths: [
|
|
||||||
3, // space
|
|
||||||
3, // !
|
|
||||||
4, // "
|
|
||||||
6, // #
|
|
||||||
6, // $
|
|
||||||
9, // %
|
|
||||||
7, // &
|
|
||||||
2, // '
|
|
||||||
4, // (
|
|
||||||
4, // )
|
|
||||||
5, // *
|
|
||||||
6, // +
|
|
||||||
3, // ,
|
|
||||||
4, // -
|
|
||||||
3, // .
|
|
||||||
4, // /
|
|
||||||
6, // 0
|
|
||||||
6, // 1
|
|
||||||
6, // 2
|
|
||||||
6, // 3
|
|
||||||
6, // 4
|
|
||||||
6, // 5
|
|
||||||
6, // 6
|
|
||||||
6, // 7
|
|
||||||
6, // 8
|
|
||||||
6, // 9
|
|
||||||
3, // :
|
|
||||||
3, // ;
|
|
||||||
6, // <
|
|
||||||
6, // =
|
|
||||||
6, // >
|
|
||||||
6, // ?
|
|
||||||
11, // @
|
|
||||||
8, // A
|
|
||||||
7, // B
|
|
||||||
7, // C
|
|
||||||
7, // D
|
|
||||||
6, // E
|
|
||||||
6, // F
|
|
||||||
8, // G
|
|
||||||
7, // H
|
|
||||||
3, // I
|
|
||||||
5, // J
|
|
||||||
7, // K
|
|
||||||
6, // L
|
|
||||||
9, // M
|
|
||||||
7, // N
|
|
||||||
8, // O
|
|
||||||
6, // P
|
|
||||||
8, // Q
|
|
||||||
7, // R
|
|
||||||
6, // S
|
|
||||||
6, // T
|
|
||||||
7, // U
|
|
||||||
7, // V
|
|
||||||
11, // W
|
|
||||||
7, // X
|
|
||||||
7, // Y
|
|
||||||
6, // Z
|
|
||||||
4, // [
|
|
||||||
4, // \
|
|
||||||
4, // ]
|
|
||||||
6, // ^
|
|
||||||
6, // _
|
|
||||||
4, // `
|
|
||||||
6, // a
|
|
||||||
6, // b
|
|
||||||
5, // c
|
|
||||||
6, // d
|
|
||||||
6, // e
|
|
||||||
4, // f
|
|
||||||
6, // g
|
|
||||||
6, // h
|
|
||||||
3, // i
|
|
||||||
3, // j
|
|
||||||
6, // k
|
|
||||||
3, // l
|
|
||||||
9, // m
|
|
||||||
6, // n
|
|
||||||
6, // o
|
|
||||||
6, // p
|
|
||||||
6, // q
|
|
||||||
4, // r
|
|
||||||
5, // s
|
|
||||||
4, // t
|
|
||||||
6, // u
|
|
||||||
6, // v
|
|
||||||
9, // w
|
|
||||||
6, // x
|
|
||||||
6, // y
|
|
||||||
5, // z
|
|
||||||
4, // {
|
|
||||||
3, // |
|
|
||||||
4, // }
|
|
||||||
6, // ~
|
|
||||||
],
|
|
||||||
data: new Uint8Array([
|
|
||||||
// Space (32)
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ! (33)
|
|
||||||
0x00, 0x00, 0xbe, 0x00, 0x00, 0x00,
|
|
||||||
// " (34)
|
|
||||||
0x00, 0x0e, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// # (35)
|
|
||||||
0x00, 0x48, 0xf8, 0x4e, 0xf8, 0x4e, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// $ (36)
|
|
||||||
0x00, 0x9c, 0x92, 0xff, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// % (37)
|
|
||||||
0x0c, 0x12, 0x12, 0x8c, 0x60, 0x18, 0xc6, 0x20, 0x20, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// & (38)
|
|
||||||
0x00, 0x60, 0x9c, 0x92, 0x6a, 0xa4, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// ' (39)
|
|
||||||
0x00, 0x0e, 0x00, 0x00,
|
|
||||||
// ( (40)
|
|
||||||
0x00, 0x7c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ) (41)
|
|
||||||
0x00, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// * (42)
|
|
||||||
0x14, 0x08, 0x3e, 0x08, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// + (43)
|
|
||||||
0x10, 0x10, 0x7c, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// , (44)
|
|
||||||
0x00, 0x00, 0xc0, 0x00, 0x00, 0x00,
|
|
||||||
// - (45)
|
|
||||||
0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// . (46)
|
|
||||||
0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
|
|
||||||
// / (47)
|
|
||||||
0x00, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// 0 (48)
|
|
||||||
0x7c, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// 1 (49)
|
|
||||||
0x00, 0x04, 0x02, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// 2 (50)
|
|
||||||
0x84, 0xc2, 0xa2, 0x92, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// 3 (51)
|
|
||||||
0x44, 0x82, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// 4 (52)
|
|
||||||
0x30, 0x28, 0x24, 0xfe, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// 5 (53)
|
|
||||||
0x4e, 0x8a, 0x8a, 0x8a, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// 6 (54)
|
|
||||||
0x78, 0x94, 0x92, 0x92, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// 7 (55)
|
|
||||||
0x02, 0xc2, 0x32, 0x0a, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// 8 (56)
|
|
||||||
0x6c, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// 9 (57)
|
|
||||||
0x0c, 0x92, 0x92, 0x52, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// : (58)
|
|
||||||
0x00, 0x00, 0x88, 0x00, 0x00, 0x00,
|
|
||||||
// ; (59)
|
|
||||||
0x00, 0x00, 0xc8, 0x00, 0x00, 0x00,
|
|
||||||
// < (60)
|
|
||||||
0x10, 0x28, 0x28, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// = (61)
|
|
||||||
0x28, 0x28, 0x28, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// > (62)
|
|
||||||
0x44, 0x44, 0x28, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ? (63)
|
|
||||||
0x04, 0x02, 0xa2, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// @ (64)
|
|
||||||
0xf0, 0x08, 0xe4, 0x12, 0x12, 0x12, 0xe2, 0x14, 0x08, 0xf0, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// A (65)
|
|
||||||
0x00, 0xc0, 0x38, 0x26, 0x26, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// B (66)
|
|
||||||
0xfe, 0x92, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// C (67)
|
|
||||||
0x7c, 0x82, 0x82, 0x82, 0x82, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// D (68)
|
|
||||||
0xfe, 0x82, 0x82, 0x82, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// E (69)
|
|
||||||
0xfe, 0x92, 0x92, 0x92, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// F (70)
|
|
||||||
0xfe, 0x12, 0x12, 0x12, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// G (71)
|
|
||||||
0x7c, 0x82, 0x82, 0x92, 0x92, 0x74, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// H (72)
|
|
||||||
0xfe, 0x10, 0x10, 0x10, 0x10, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// I (73)
|
|
||||||
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// J (74)
|
|
||||||
0x40, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// K (75)
|
|
||||||
0xfe, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// L (76)
|
|
||||||
0xfe, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// M (77)
|
|
||||||
0xfe, 0x04, 0x18, 0x60, 0x60, 0x18, 0x04, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// N (78)
|
|
||||||
0xfe, 0x04, 0x08, 0x30, 0x40, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// O (79)
|
|
||||||
0x7c, 0x82, 0x82, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// P (80)
|
|
||||||
0xfe, 0x12, 0x12, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// Q (81)
|
|
||||||
0x7c, 0x82, 0x82, 0x82, 0x42, 0xc2, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
// R (82)
|
|
||||||
0xfe, 0x12, 0x12, 0x32, 0x4c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// S (83)
|
|
||||||
0x4c, 0x92, 0x92, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// T (84)
|
|
||||||
0x02, 0x02, 0xfe, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// U (85)
|
|
||||||
0x7e, 0x80, 0x80, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// V (86)
|
|
||||||
0x06, 0x18, 0x60, 0x80, 0x60, 0x18, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// W (87)
|
|
||||||
0x06, 0x38, 0xc0, 0x38, 0x06, 0x38, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// X (88)
|
|
||||||
0x82, 0x44, 0x28, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// Y (89)
|
|
||||||
0x02, 0x04, 0x08, 0xf0, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
// Z (90)
|
|
||||||
0xc2, 0xa2, 0x92, 0x8a, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// [ (91)
|
|
||||||
0x00, 0xfe, 0x82, 0x82, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// \ (92)
|
|
||||||
0x06, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ] (93)
|
|
||||||
0x82, 0x82, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ^ (94)
|
|
||||||
0x08, 0x04, 0x02, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// _ (95)
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ` (96)
|
|
||||||
0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// a (97)
|
|
||||||
0x40, 0xa8, 0xa8, 0xa8, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// b (98)
|
|
||||||
0xfe, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// c (99)
|
|
||||||
0x70, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// d (100)
|
|
||||||
0x70, 0x88, 0x88, 0x88, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// e (101)
|
|
||||||
0x70, 0xa8, 0xa8, 0xa8, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// f (102)
|
|
||||||
0x08, 0xfc, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// g (103)
|
|
||||||
0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00,
|
|
||||||
// h (104)
|
|
||||||
0xfe, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// i (105)
|
|
||||||
0x00, 0xfa, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// j (106)
|
|
||||||
0x00, 0x00, 0xfa, 0x01, 0x00, 0x00,
|
|
||||||
// k (107)
|
|
||||||
0xfe, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// l (108)
|
|
||||||
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// m (109)
|
|
||||||
0xf8, 0x08, 0x08, 0xf0, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// n (110)
|
|
||||||
0xf8, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// o (111)
|
|
||||||
0x70, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// p (112)
|
|
||||||
0xf8, 0x48, 0x48, 0x48, 0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// q (113)
|
|
||||||
0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
|
||||||
// r (114)
|
|
||||||
0xf8, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// s (115)
|
|
||||||
0x90, 0xa8, 0xa8, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// t (116)
|
|
||||||
0x08, 0x7e, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// u (117)
|
|
||||||
0x78, 0x80, 0x80, 0x80, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// v (118)
|
|
||||||
0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// w (119)
|
|
||||||
0x78, 0x80, 0x60, 0x18, 0x60, 0x80, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// x (120)
|
|
||||||
0x88, 0x50, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// y (121)
|
|
||||||
0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// z (122)
|
|
||||||
0xc8, 0xa8, 0x98, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// { (123)
|
|
||||||
0x10, 0x6c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// | (124)
|
|
||||||
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// } (125)
|
|
||||||
0x82, 0x6c, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ~ (126)
|
|
||||||
0x10, 0x08, 0x10, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ArialMT Plain 16 - Medium font (height ~19 pixels)
|
|
||||||
* Simplified subset covering 0-9, A-Z, a-z, common punctuation
|
|
||||||
*/
|
|
||||||
export const ArialMT_Plain_16: OLEDFont = {
|
|
||||||
height: 19,
|
|
||||||
firstChar: 32,
|
|
||||||
charCount: 95,
|
|
||||||
widths: [
|
|
||||||
4, // space
|
|
||||||
4, // !
|
|
||||||
5, // "
|
|
||||||
8, // #
|
|
||||||
8, // $
|
|
||||||
12, // %
|
|
||||||
10, // &
|
|
||||||
2, // '
|
|
||||||
5, // (
|
|
||||||
5, // )
|
|
||||||
6, // *
|
|
||||||
8, // +
|
|
||||||
4, // ,
|
|
||||||
5, // -
|
|
||||||
4, // .
|
|
||||||
4, // /
|
|
||||||
8, // 0
|
|
||||||
8, // 1
|
|
||||||
8, // 2
|
|
||||||
8, // 3
|
|
||||||
8, // 4
|
|
||||||
8, // 5
|
|
||||||
8, // 6
|
|
||||||
8, // 7
|
|
||||||
8, // 8
|
|
||||||
8, // 9
|
|
||||||
4, // :
|
|
||||||
4, // ;
|
|
||||||
8, // <
|
|
||||||
8, // =
|
|
||||||
8, // >
|
|
||||||
8, // ?
|
|
||||||
15, // @
|
|
||||||
10, // A
|
|
||||||
9, // B
|
|
||||||
10, // C
|
|
||||||
10, // D
|
|
||||||
9, // E
|
|
||||||
8, // F
|
|
||||||
11, // G
|
|
||||||
9, // H
|
|
||||||
4, // I
|
|
||||||
6, // J
|
|
||||||
9, // K
|
|
||||||
8, // L
|
|
||||||
11, // M
|
|
||||||
9, // N
|
|
||||||
11, // O
|
|
||||||
9, // P
|
|
||||||
11, // Q
|
|
||||||
10, // R
|
|
||||||
9, // S
|
|
||||||
8, // T
|
|
||||||
9, // U
|
|
||||||
10, // V
|
|
||||||
14, // W
|
|
||||||
10, // X
|
|
||||||
10, // Y
|
|
||||||
9, // Z
|
|
||||||
4, // [
|
|
||||||
4, // \
|
|
||||||
4, // ]
|
|
||||||
7, // ^
|
|
||||||
8, // _
|
|
||||||
5, // `
|
|
||||||
8, // a
|
|
||||||
8, // b
|
|
||||||
7, // c
|
|
||||||
8, // d
|
|
||||||
8, // e
|
|
||||||
4, // f
|
|
||||||
8, // g
|
|
||||||
8, // h
|
|
||||||
4, // i
|
|
||||||
4, // j
|
|
||||||
8, // k
|
|
||||||
4, // l
|
|
||||||
12, // m
|
|
||||||
8, // n
|
|
||||||
8, // o
|
|
||||||
8, // p
|
|
||||||
8, // q
|
|
||||||
5, // r
|
|
||||||
7, // s
|
|
||||||
4, // t
|
|
||||||
8, // u
|
|
||||||
8, // v
|
|
||||||
12, // w
|
|
||||||
8, // x
|
|
||||||
8, // y
|
|
||||||
7, // z
|
|
||||||
5, // {
|
|
||||||
4, // |
|
|
||||||
5, // }
|
|
||||||
8, // ~
|
|
||||||
],
|
|
||||||
// Font data would be similar pattern but larger - using placeholder
|
|
||||||
data: new Uint8Array(2500).fill(0xff), // Placeholder - would contain actual font bitmap data
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to get FONT_HEIGHT_SMALL equivalent
|
|
||||||
*/
|
|
||||||
export const FONT_HEIGHT_SMALL = 13;
|
|
||||||
export const FONT_HEIGHT_MEDIUM = 19;
|
|
||||||
export const FONT_HEIGHT_LARGE = 28;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to create a font from raw byte data (for importing firmware fonts)
|
|
||||||
*/
|
|
||||||
export function createFontFromBytes(
|
|
||||||
height: number,
|
|
||||||
firstChar: number,
|
|
||||||
widths: number[],
|
|
||||||
data: number[],
|
|
||||||
): OLEDFont {
|
|
||||||
return {
|
|
||||||
height,
|
|
||||||
firstChar,
|
|
||||||
charCount: widths.length,
|
|
||||||
widths,
|
|
||||||
data: new Uint8Array(data),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
/**
|
|
||||||
* OLEDImages.ts - Icon and image data for OLED display emulator
|
|
||||||
*
|
|
||||||
* These are direct ports of the XBM/bitmap icons from the Meshtastic firmware.
|
|
||||||
* Format: XBM style (LSB first, horizontal scanning)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Satellite icon (8x8)
|
|
||||||
export const imgSatellite_width = 8;
|
|
||||||
export const imgSatellite_height = 8;
|
|
||||||
export const imgSatellite = new Uint8Array([
|
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111,
|
|
||||||
0b11011011, 0b00011000,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// USB icon (10x8)
|
|
||||||
export const imgUSB = new Uint8Array([
|
|
||||||
0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe, 0x85, 0xfe, 0x89, 0xff, 0xf1,
|
|
||||||
0xfc, 0x00, 0xfc,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Power icon (8x16)
|
|
||||||
export const imgPower = new Uint8Array([
|
|
||||||
0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1c, 0x22, 0x22, 0x41, 0x7f,
|
|
||||||
0x22, 0x22, 0x22,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// User icon (8x8)
|
|
||||||
export const imgUser = new Uint8Array([
|
|
||||||
0x3c, 0x42, 0x99, 0xa5, 0xa5, 0x99, 0x42, 0x3c,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Position empty arrow (6x8)
|
|
||||||
export const imgPositionEmpty = new Uint8Array([
|
|
||||||
0x20, 0x30, 0x28, 0x24, 0x42, 0xff,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Position solid arrow (6x8)
|
|
||||||
export const imgPositionSolid = new Uint8Array([
|
|
||||||
0x20, 0x30, 0x38, 0x3c, 0x7e, 0xff,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Mail icon (10x7)
|
|
||||||
export const mail_width = 10;
|
|
||||||
export const mail_height = 7;
|
|
||||||
export const mail = new Uint8Array([
|
|
||||||
0b11111111,
|
|
||||||
0b00, // Top line
|
|
||||||
0b10000001,
|
|
||||||
0b00, // Edges
|
|
||||||
0b11000011,
|
|
||||||
0b00, // Diagonals start
|
|
||||||
0b10100101,
|
|
||||||
0b00, // Inner M part
|
|
||||||
0b10011001,
|
|
||||||
0b00, // Inner M part
|
|
||||||
0b10000001,
|
|
||||||
0b00, // Edges
|
|
||||||
0b11111111,
|
|
||||||
0b00, // Bottom line
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Hop icon (9x10)
|
|
||||||
export const hop_width = 9;
|
|
||||||
export const hop_height = 10;
|
|
||||||
export const hop = new Uint8Array([
|
|
||||||
0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, 0x00, 0xc0,
|
|
||||||
0x01, 0x40, 0x01, 0xc0, 0x01, 0x40, 0x00,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Mail icon (8x8)
|
|
||||||
export const icon_mail = new Uint8Array([
|
|
||||||
0b11111111, // ████████ top border
|
|
||||||
0b10000001, // █ █ sides
|
|
||||||
0b11000011, // ██ ██ diagonal
|
|
||||||
0b10100101, // █ █ █ █ inner M
|
|
||||||
0b10011001, // █ ██ █ inner M
|
|
||||||
0b10000001, // █ █ sides
|
|
||||||
0b10000001, // █ █ sides
|
|
||||||
0b11111111, // ████████ bottom
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Compass icon (8x8)
|
|
||||||
export const icon_compass = new Uint8Array([
|
|
||||||
0x3c, // Row 0: ..####..
|
|
||||||
0x52, // Row 1: .#..#.#.
|
|
||||||
0x91, // Row 2: #...#..#
|
|
||||||
0x91, // Row 3: #...#..#
|
|
||||||
0x91, // Row 4: #...#..#
|
|
||||||
0x81, // Row 5: #......#
|
|
||||||
0x42, // Row 6: .#....#.
|
|
||||||
0x3c, // Row 7: ..####..
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Radio icon (8x8)
|
|
||||||
export const icon_radio = new Uint8Array([
|
|
||||||
0x0f, // Row 0: ####....
|
|
||||||
0x10, // Row 1: ....#...
|
|
||||||
0x27, // Row 2: ###..#..
|
|
||||||
0x48, // Row 3: ...#..#.
|
|
||||||
0x93, // Row 4: ##..#..#
|
|
||||||
0xa4, // Row 5: ..#..#.#
|
|
||||||
0xa8, // Row 6: ...#.#.#
|
|
||||||
0xa9, // Row 7: #..#.#.#
|
|
||||||
]);
|
|
||||||
|
|
||||||
// System/settings icon (8x8)
|
|
||||||
export const icon_system = new Uint8Array([
|
|
||||||
0x24, // Row 0: ..#..#..
|
|
||||||
0x3c, // Row 1: ..####..
|
|
||||||
0xc3, // Row 2: ##....##
|
|
||||||
0x5a, // Row 3: .#.##.#.
|
|
||||||
0x5a, // Row 4: .#.##.#.
|
|
||||||
0xc3, // Row 5: ##....##
|
|
||||||
0x3c, // Row 6: ..####..
|
|
||||||
0x24, // Row 7: ..#..#..
|
|
||||||
]);
|
|
||||||
|
|
||||||
// WiFi icon (8x8)
|
|
||||||
export const icon_wifi = new Uint8Array([
|
|
||||||
0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000,
|
|
||||||
0b00011000, 0b00000000,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Nodes icon (8x8)
|
|
||||||
export const icon_nodes = new Uint8Array([
|
|
||||||
0xf9, // Row 0: #..#####
|
|
||||||
0x00, // Row 1
|
|
||||||
0xf9, // Row 2: #..#####
|
|
||||||
0x00, // Row 3
|
|
||||||
0xf9, // Row 4: #..#####
|
|
||||||
0x00, // Row 5
|
|
||||||
0xf9, // Row 6: #..#####
|
|
||||||
0x00, // Row 7
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Battery vertical (7x11)
|
|
||||||
export const batteryBitmap_v = new Uint8Array([
|
|
||||||
0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000,
|
|
||||||
0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Battery side gaps (8x3)
|
|
||||||
export const batteryBitmap_sidegaps_v = new Uint8Array([
|
|
||||||
0b10000010, 0b10000010, 0b10000010,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Lightning bolt vertical (5x5)
|
|
||||||
export const lightning_bolt_v = new Uint8Array([
|
|
||||||
0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Horizontal battery bottom (13x9)
|
|
||||||
export const batteryBitmap_h_bottom = new Uint8Array([
|
|
||||||
0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
|
||||||
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
|
||||||
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
|
||||||
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
|
||||||
0b00011110, 0b00000000,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Horizontal battery top (13x9)
|
|
||||||
export const batteryBitmap_h_top = new Uint8Array([
|
|
||||||
0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
|
|
||||||
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b11000000, 0b00000000,
|
|
||||||
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
|
|
||||||
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
|
|
||||||
0b00111100, 0b00000000,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Lightning bolt horizontal (13x9)
|
|
||||||
export const lightning_bolt_h = new Uint8Array([
|
|
||||||
0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000,
|
|
||||||
0b00111000, 0b00000000, 0b00111100, 0b00000000, 0b00011110, 0b00000000,
|
|
||||||
0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000,
|
|
||||||
0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000,
|
|
||||||
0b00000000, 0b00000000,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Signal bars icon (various signal strengths)
|
|
||||||
export const signal_bars = {
|
|
||||||
width: 12,
|
|
||||||
height: 8,
|
|
||||||
// 0 bars
|
|
||||||
none: new Uint8Array([
|
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
|
||||||
]),
|
|
||||||
// 1 bar
|
|
||||||
low: new Uint8Array([
|
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
|
||||||
0b00000011, 0b00000000, 0b00000011, 0b00000000,
|
|
||||||
]),
|
|
||||||
// 2 bars
|
|
||||||
medium: new Uint8Array([
|
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
|
||||||
0b00000000, 0b00000000, 0b00001100, 0b00000000, 0b00001100, 0b00000000,
|
|
||||||
0b00001111, 0b00000000, 0b00001111, 0b00000000,
|
|
||||||
]),
|
|
||||||
// 3 bars
|
|
||||||
high: new Uint8Array([
|
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00110000, 0b00000000,
|
|
||||||
0b00110000, 0b00000000, 0b00111100, 0b00000000, 0b00111100, 0b00000000,
|
|
||||||
0b00111111, 0b00000000, 0b00111111, 0b00000000,
|
|
||||||
]),
|
|
||||||
// 4 bars (full)
|
|
||||||
full: new Uint8Array([
|
|
||||||
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11110000, 0b00000000,
|
|
||||||
0b11110000, 0b00000000, 0b11111100, 0b00000000, 0b11111100, 0b00000000,
|
|
||||||
0b11111111, 0b00000000, 0b11111111, 0b00000000,
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Meshtastic logo (simplified 32x32)
|
|
||||||
export const logo_width = 32;
|
|
||||||
export const logo_height = 32;
|
|
||||||
export const meshtastic_logo = new Uint8Array([
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00,
|
|
||||||
0x03, 0xc0, 0x00, 0x80, 0x01, 0x80, 0x01, 0xc0, 0x00, 0x00, 0x03, 0x60, 0x00,
|
|
||||||
0x00, 0x06, 0x30, 0x00, 0x00, 0x0c, 0x18, 0xf8, 0x1f, 0x18, 0x08, 0x04, 0x20,
|
|
||||||
0x10, 0x0c, 0x02, 0x40, 0x30, 0x04, 0x01, 0x80, 0x20, 0x06, 0x01, 0x80, 0x60,
|
|
||||||
0x02, 0xe1, 0x87, 0x40, 0x02, 0x11, 0x88, 0x40, 0x03, 0x09, 0x90, 0xc0, 0x03,
|
|
||||||
0x09, 0x90, 0xc0, 0x02, 0x11, 0x88, 0x40, 0x02, 0xe1, 0x87, 0x40, 0x06, 0x01,
|
|
||||||
0x80, 0x60, 0x04, 0x01, 0x80, 0x20, 0x0c, 0x02, 0x40, 0x30, 0x08, 0x04, 0x20,
|
|
||||||
0x10, 0x18, 0xf8, 0x1f, 0x18, 0x30, 0x00, 0x00, 0x0c, 0x60, 0x00, 0x00, 0x06,
|
|
||||||
0xc0, 0x00, 0x00, 0x03, 0x80, 0x01, 0x80, 0x01, 0x00, 0x03, 0xc0, 0x00, 0x00,
|
|
||||||
0x06, 0x60, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Bluetooth icon (8x11)
|
|
||||||
export const icon_bluetooth = new Uint8Array([
|
|
||||||
0b00000000, 0b10000100, 0b01001010, 0b00101010, 0b00010100, 0b11111111,
|
|
||||||
0b00010100, 0b00101010, 0b01001010, 0b10000100, 0b00000000,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// GPS/Location pin icon (8x12)
|
|
||||||
export const icon_gps_pin = new Uint8Array([
|
|
||||||
0b00111100, 0b01111110, 0b11111111, 0b11100111, 0b11100111, 0b11111111,
|
|
||||||
0b01111110, 0b00111100, 0b00011000, 0b00011000, 0b00001000, 0b00000000,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Message/chat bubble icon (10x8)
|
|
||||||
export const icon_message = new Uint8Array([
|
|
||||||
0b11111111, 0b00, 0b10000001, 0b00, 0b10000001, 0b00, 0b10000001, 0b00,
|
|
||||||
0b10000001, 0b00, 0b11111111, 0b00, 0b00000110, 0b00, 0b00000010, 0b00,
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to convert icon data to XBM format if needed
|
|
||||||
*/
|
|
||||||
export function toXbm(data: Uint8Array | number[]): Uint8Array {
|
|
||||||
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
||||||
}
|
|
||||||
@@ -1,627 +0,0 @@
|
|||||||
/**
|
|
||||||
* ScreenRenderers.ts - Sample screen rendering functions
|
|
||||||
*
|
|
||||||
* These demonstrate how to port Meshtastic firmware UI screens
|
|
||||||
* to the OLED emulator. The API matches the firmware's drawing code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
OLEDDisplay,
|
|
||||||
WHITE,
|
|
||||||
BLACK,
|
|
||||||
TEXT_ALIGN_LEFT,
|
|
||||||
TEXT_ALIGN_CENTER,
|
|
||||||
TEXT_ALIGN_RIGHT,
|
|
||||||
} from "./OLEDDisplay";
|
|
||||||
import { Font_6x8 } from "./SimpleFonts";
|
|
||||||
import * as images from "./OLEDImages";
|
|
||||||
|
|
||||||
// Font height for simple 6x8 font
|
|
||||||
const FONT_HEIGHT_SMALL = 8;
|
|
||||||
|
|
||||||
// Screen resolution detection (matches SharedUIDisplay.cpp)
|
|
||||||
export type ScreenResolution = "UltraLow" | "Low" | "High";
|
|
||||||
|
|
||||||
export function determineScreenResolution(
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
): ScreenResolution {
|
|
||||||
if (width <= 64 || height <= 48) {
|
|
||||||
return "UltraLow";
|
|
||||||
}
|
|
||||||
if (width > 128) {
|
|
||||||
return "High";
|
|
||||||
}
|
|
||||||
return "Low";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for consistent line positioning
|
|
||||||
export function getTextPositions(display: OLEDDisplay): number[] {
|
|
||||||
const fontHeight = FONT_HEIGHT_SMALL;
|
|
||||||
return [
|
|
||||||
0, // textZeroLine
|
|
||||||
fontHeight - 1, // textFirstLine
|
|
||||||
fontHeight - 1 + (fontHeight - 5), // textSecondLine
|
|
||||||
fontHeight - 1 + 2 * (fontHeight - 5), // textThirdLine
|
|
||||||
fontHeight - 1 + 3 * (fontHeight - 5), // textFourthLine
|
|
||||||
fontHeight - 1 + 4 * (fontHeight - 5), // textFifthLine
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a rounded highlight box (used for inverted headers)
|
|
||||||
*/
|
|
||||||
export function drawRoundedHighlight(
|
|
||||||
display: OLEDDisplay,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
w: number,
|
|
||||||
h: number,
|
|
||||||
r: number,
|
|
||||||
): void {
|
|
||||||
// Draw the center and side rectangles
|
|
||||||
display.fillRect(x + r, y, w - 2 * r, h); // center bar
|
|
||||||
display.fillRect(x, y + r, r, h - 2 * r); // left edge
|
|
||||||
display.fillRect(x + w - r, y + r, r, h - 2 * r); // right edge
|
|
||||||
|
|
||||||
// Draw the rounded corners using filled circles
|
|
||||||
display.fillCircle(x + r + 1, y + r, r); // top-left
|
|
||||||
display.fillCircle(x + w - r - 1, y + r, r); // top-right
|
|
||||||
display.fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left
|
|
||||||
display.fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw the common header bar (battery, title, icons)
|
|
||||||
*/
|
|
||||||
export function drawCommonHeader(
|
|
||||||
display: OLEDDisplay,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
titleStr: string,
|
|
||||||
options: {
|
|
||||||
inverted?: boolean;
|
|
||||||
batteryPercent?: number;
|
|
||||||
isCharging?: boolean;
|
|
||||||
hasUSB?: boolean;
|
|
||||||
hasUnreadMessage?: boolean;
|
|
||||||
} = {},
|
|
||||||
): void {
|
|
||||||
const {
|
|
||||||
inverted = true,
|
|
||||||
batteryPercent = 75,
|
|
||||||
isCharging = false,
|
|
||||||
hasUSB = false,
|
|
||||||
hasUnreadMessage = false,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const HEADER_OFFSET_Y = 1;
|
|
||||||
y += HEADER_OFFSET_Y;
|
|
||||||
|
|
||||||
display.setFont(Font_6x8);
|
|
||||||
const highlightHeight = FONT_HEIGHT_SMALL - 1;
|
|
||||||
const screenW = display.getWidth();
|
|
||||||
|
|
||||||
if (inverted) {
|
|
||||||
// Draw inverted header background
|
|
||||||
display.setColor(WHITE);
|
|
||||||
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
|
||||||
display.setColor(BLACK);
|
|
||||||
} else {
|
|
||||||
// Draw line under header
|
|
||||||
display.setColor(WHITE);
|
|
||||||
display.drawLine(0, 14, screenW, 14);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw title centered
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
|
||||||
display.drawString(screenW / 2, y, titleStr);
|
|
||||||
|
|
||||||
// Reset color for remaining elements
|
|
||||||
if (inverted) {
|
|
||||||
display.setColor(WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw battery on left
|
|
||||||
let batteryX = 1;
|
|
||||||
const batteryY = HEADER_OFFSET_Y + 1;
|
|
||||||
|
|
||||||
if (hasUSB && !isCharging) {
|
|
||||||
// Draw USB icon
|
|
||||||
display.drawXbm(batteryX + 1, batteryY + 2, 10, 8, images.imgUSB);
|
|
||||||
batteryX += 11;
|
|
||||||
} else {
|
|
||||||
// Draw battery outline
|
|
||||||
display.drawXbm(batteryX, batteryY, 7, 11, images.batteryBitmap_v);
|
|
||||||
|
|
||||||
if (isCharging) {
|
|
||||||
// Draw lightning bolt
|
|
||||||
display.drawXbm(
|
|
||||||
batteryX + 1,
|
|
||||||
batteryY + 3,
|
|
||||||
5,
|
|
||||||
5,
|
|
||||||
images.lightning_bolt_v,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Draw battery level
|
|
||||||
display.drawXbm(
|
|
||||||
batteryX - 1,
|
|
||||||
batteryY + 4,
|
|
||||||
8,
|
|
||||||
3,
|
|
||||||
images.batteryBitmap_sidegaps_v,
|
|
||||||
);
|
|
||||||
const fillHeight = Math.floor((8 * batteryPercent) / 100);
|
|
||||||
const fillY = batteryY - fillHeight + 10;
|
|
||||||
display.fillRect(batteryX + 1, fillY, 5, fillHeight);
|
|
||||||
}
|
|
||||||
batteryX += 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw mail icon on right if unread message
|
|
||||||
if (hasUnreadMessage) {
|
|
||||||
const mailX = screenW - images.mail_width - 2;
|
|
||||||
display.drawXbm(
|
|
||||||
mailX,
|
|
||||||
y + 2,
|
|
||||||
images.mail_width,
|
|
||||||
images.mail_height,
|
|
||||||
images.mail,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
display.setColor(WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a boot/splash screen with logo
|
|
||||||
*/
|
|
||||||
export function drawBootScreen(
|
|
||||||
display: OLEDDisplay,
|
|
||||||
appVersion: string = "2.5.0",
|
|
||||||
): void {
|
|
||||||
display.clear();
|
|
||||||
display.setFont(Font_6x8);
|
|
||||||
display.setColor(WHITE);
|
|
||||||
|
|
||||||
const centerX = display.getWidth() / 2;
|
|
||||||
const centerY = display.getHeight() / 2;
|
|
||||||
|
|
||||||
// Draw logo centered
|
|
||||||
const logoX = centerX - images.logo_width / 2;
|
|
||||||
const logoY = centerY - images.logo_height / 2 - 8;
|
|
||||||
display.drawXbm(
|
|
||||||
logoX,
|
|
||||||
logoY,
|
|
||||||
images.logo_width,
|
|
||||||
images.logo_height,
|
|
||||||
images.meshtastic_logo,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Draw version below
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
|
||||||
display.drawString(
|
|
||||||
centerX,
|
|
||||||
centerY + images.logo_height / 2,
|
|
||||||
`v${appVersion}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a node info screen
|
|
||||||
*/
|
|
||||||
export function drawNodeInfoScreen(
|
|
||||||
display: OLEDDisplay,
|
|
||||||
nodeInfo: {
|
|
||||||
shortName: string;
|
|
||||||
longName: string;
|
|
||||||
nodeId: string;
|
|
||||||
batteryLevel?: number;
|
|
||||||
lastHeard?: string;
|
|
||||||
snr?: number;
|
|
||||||
hopsAway?: number;
|
|
||||||
},
|
|
||||||
): void {
|
|
||||||
display.clear();
|
|
||||||
|
|
||||||
// Draw header
|
|
||||||
drawCommonHeader(display, 0, 0, "Node Info", {
|
|
||||||
inverted: true,
|
|
||||||
batteryPercent: 85,
|
|
||||||
});
|
|
||||||
|
|
||||||
display.setFont(Font_6x8);
|
|
||||||
display.setColor(WHITE);
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
|
|
||||||
const lines = getTextPositions(display);
|
|
||||||
const screenW = display.getWidth();
|
|
||||||
|
|
||||||
// Node name (bolded by drawing twice offset)
|
|
||||||
display.drawString(0, lines[1], nodeInfo.longName);
|
|
||||||
display.drawString(1, lines[1], nodeInfo.longName);
|
|
||||||
|
|
||||||
// Short name and ID
|
|
||||||
display.drawString(0, lines[2], `${nodeInfo.shortName} • ${nodeInfo.nodeId}`);
|
|
||||||
|
|
||||||
// Last heard
|
|
||||||
if (nodeInfo.lastHeard) {
|
|
||||||
display.drawString(0, lines[3], `Heard: ${nodeInfo.lastHeard}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signal info on right side
|
|
||||||
if (nodeInfo.snr !== undefined) {
|
|
||||||
const snrStr = `SNR: ${nodeInfo.snr}dB`;
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
|
||||||
display.drawString(screenW, lines[3], snrStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hops
|
|
||||||
if (nodeInfo.hopsAway !== undefined) {
|
|
||||||
const hopStr =
|
|
||||||
nodeInfo.hopsAway === 0 ? "Direct" : `${nodeInfo.hopsAway} hops`;
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
display.drawString(0, lines[4], hopStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a message screen
|
|
||||||
*/
|
|
||||||
export function drawMessageScreen(
|
|
||||||
display: OLEDDisplay,
|
|
||||||
message: {
|
|
||||||
from: string;
|
|
||||||
text: string;
|
|
||||||
time: string;
|
|
||||||
channel?: string;
|
|
||||||
},
|
|
||||||
): void {
|
|
||||||
display.clear();
|
|
||||||
|
|
||||||
// Draw header with channel name if provided
|
|
||||||
const headerTitle = message.channel || "Message";
|
|
||||||
drawCommonHeader(display, 0, 0, headerTitle, {
|
|
||||||
inverted: true,
|
|
||||||
batteryPercent: 72,
|
|
||||||
hasUnreadMessage: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
display.setFont(Font_6x8);
|
|
||||||
display.setColor(WHITE);
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
|
|
||||||
const lines = getTextPositions(display);
|
|
||||||
const screenW = display.getWidth();
|
|
||||||
|
|
||||||
// From line
|
|
||||||
display.drawString(0, lines[1], `From: ${message.from}`);
|
|
||||||
|
|
||||||
// Time on right
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
|
||||||
display.drawString(screenW, lines[1], message.time);
|
|
||||||
|
|
||||||
// Message text (with word wrap)
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
display.drawStringMaxWidth(0, lines[2], screenW, message.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a GPS/location screen
|
|
||||||
*/
|
|
||||||
export function drawGPSScreen(
|
|
||||||
display: OLEDDisplay,
|
|
||||||
gpsInfo: {
|
|
||||||
latitude?: number;
|
|
||||||
longitude?: number;
|
|
||||||
altitude?: number;
|
|
||||||
satellites?: number;
|
|
||||||
hasLock: boolean;
|
|
||||||
speed?: number;
|
|
||||||
},
|
|
||||||
): void {
|
|
||||||
display.clear();
|
|
||||||
|
|
||||||
drawCommonHeader(display, 0, 0, "GPS", {
|
|
||||||
inverted: true,
|
|
||||||
batteryPercent: 90,
|
|
||||||
});
|
|
||||||
|
|
||||||
display.setFont(Font_6x8);
|
|
||||||
display.setColor(WHITE);
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
|
|
||||||
const lines = getTextPositions(display);
|
|
||||||
|
|
||||||
// Draw satellite icon and count
|
|
||||||
display.drawXbm(
|
|
||||||
0,
|
|
||||||
lines[1],
|
|
||||||
images.imgSatellite_width,
|
|
||||||
images.imgSatellite_height,
|
|
||||||
images.imgSatellite,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!gpsInfo.hasLock) {
|
|
||||||
display.drawString(12, lines[1], "No Lock");
|
|
||||||
} else {
|
|
||||||
display.drawString(12, lines[1], `${gpsInfo.satellites || 0} sats`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
gpsInfo.hasLock &&
|
|
||||||
gpsInfo.latitude !== undefined &&
|
|
||||||
gpsInfo.longitude !== undefined
|
|
||||||
) {
|
|
||||||
// Coordinates
|
|
||||||
display.drawString(0, lines[2], `Lat: ${gpsInfo.latitude.toFixed(6)}`);
|
|
||||||
display.drawString(0, lines[3], `Lon: ${gpsInfo.longitude.toFixed(6)}`);
|
|
||||||
|
|
||||||
// Altitude
|
|
||||||
if (gpsInfo.altitude !== undefined) {
|
|
||||||
display.drawString(0, lines[4], `Alt: ${gpsInfo.altitude.toFixed(0)}m`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speed
|
|
||||||
if (gpsInfo.speed !== undefined) {
|
|
||||||
const screenW = display.getWidth();
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
|
||||||
display.drawString(screenW, lines[4], `${gpsInfo.speed.toFixed(1)} km/h`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a node list screen
|
|
||||||
*/
|
|
||||||
export function drawNodeListScreen(
|
|
||||||
display: OLEDDisplay,
|
|
||||||
nodes: Array<{
|
|
||||||
shortName: string;
|
|
||||||
longName: string;
|
|
||||||
lastHeard: string;
|
|
||||||
snr?: number;
|
|
||||||
}>,
|
|
||||||
selectedIndex: number = 0,
|
|
||||||
): void {
|
|
||||||
display.clear();
|
|
||||||
|
|
||||||
drawCommonHeader(display, 0, 0, `Nodes (${nodes.length})`, {
|
|
||||||
inverted: true,
|
|
||||||
batteryPercent: 80,
|
|
||||||
});
|
|
||||||
|
|
||||||
display.setFont(Font_6x8);
|
|
||||||
display.setColor(WHITE);
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
|
|
||||||
const lines = getTextPositions(display);
|
|
||||||
const screenW = display.getWidth();
|
|
||||||
const maxVisibleNodes = 4;
|
|
||||||
const startIndex = Math.max(
|
|
||||||
0,
|
|
||||||
selectedIndex - Math.floor(maxVisibleNodes / 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = 0; i < maxVisibleNodes && startIndex + i < nodes.length; i++) {
|
|
||||||
const node = nodes[startIndex + i];
|
|
||||||
const lineY = lines[i + 1];
|
|
||||||
const isSelected = startIndex + i === selectedIndex;
|
|
||||||
|
|
||||||
// Highlight selected row
|
|
||||||
if (isSelected) {
|
|
||||||
display.fillRect(0, lineY - 1, screenW, FONT_HEIGHT_SMALL);
|
|
||||||
display.setColor(BLACK);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node short name
|
|
||||||
display.drawString(0, lineY, node.shortName);
|
|
||||||
|
|
||||||
// Last heard on right
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
|
||||||
display.drawString(screenW, lineY, node.lastHeard);
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
display.setColor(WHITE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a system info screen
|
|
||||||
*/
|
|
||||||
export function drawSystemScreen(
|
|
||||||
display: OLEDDisplay,
|
|
||||||
sysInfo: {
|
|
||||||
uptime: string;
|
|
||||||
channelUtil: number;
|
|
||||||
airUtil: number;
|
|
||||||
batteryVoltage?: number;
|
|
||||||
nodes: number;
|
|
||||||
freeMemory?: number;
|
|
||||||
},
|
|
||||||
): void {
|
|
||||||
display.clear();
|
|
||||||
|
|
||||||
drawCommonHeader(display, 0, 0, "System", {
|
|
||||||
inverted: true,
|
|
||||||
batteryPercent: 95,
|
|
||||||
});
|
|
||||||
|
|
||||||
display.setFont(Font_6x8);
|
|
||||||
display.setColor(WHITE);
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
|
|
||||||
const lines = getTextPositions(display);
|
|
||||||
const screenW = display.getWidth();
|
|
||||||
|
|
||||||
// Uptime
|
|
||||||
display.drawString(0, lines[1], `Uptime: ${sysInfo.uptime}`);
|
|
||||||
|
|
||||||
// Channel utilization
|
|
||||||
display.drawString(0, lines[2], `ChUtil: ${sysInfo.channelUtil.toFixed(1)}%`);
|
|
||||||
|
|
||||||
// Air utilization
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
|
||||||
display.drawString(
|
|
||||||
screenW,
|
|
||||||
lines[2],
|
|
||||||
`AirTx: ${sysInfo.airUtil.toFixed(1)}%`,
|
|
||||||
);
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
|
|
||||||
// Nodes
|
|
||||||
display.drawString(0, lines[3], `Nodes: ${sysInfo.nodes}`);
|
|
||||||
|
|
||||||
// Battery voltage
|
|
||||||
if (sysInfo.batteryVoltage !== undefined) {
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
|
||||||
display.drawString(
|
|
||||||
screenW,
|
|
||||||
lines[3],
|
|
||||||
`${sysInfo.batteryVoltage.toFixed(2)}V`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Free memory
|
|
||||||
if (sysInfo.freeMemory !== undefined) {
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
display.drawString(
|
|
||||||
0,
|
|
||||||
lines[4],
|
|
||||||
`Free: ${(sysInfo.freeMemory / 1024).toFixed(0)}KB`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a compass screen with bearing arrow
|
|
||||||
*/
|
|
||||||
export function drawCompassScreen(
|
|
||||||
display: OLEDDisplay,
|
|
||||||
compassInfo: {
|
|
||||||
heading: number; // Device heading in degrees
|
|
||||||
bearing: number; // Bearing to target in degrees
|
|
||||||
distance?: number; // Distance to target in meters
|
|
||||||
targetName?: string;
|
|
||||||
},
|
|
||||||
): void {
|
|
||||||
display.clear();
|
|
||||||
|
|
||||||
drawCommonHeader(display, 0, 0, compassInfo.targetName || "Compass", {
|
|
||||||
inverted: true,
|
|
||||||
batteryPercent: 70,
|
|
||||||
});
|
|
||||||
|
|
||||||
display.setFont(Font_6x8);
|
|
||||||
display.setColor(WHITE);
|
|
||||||
|
|
||||||
const screenW = display.getWidth();
|
|
||||||
const screenH = display.getHeight();
|
|
||||||
|
|
||||||
// Compass center and radius
|
|
||||||
const compassRadius = Math.min(screenW, screenH - 20) / 2 - 4;
|
|
||||||
const compassX = screenW / 2;
|
|
||||||
const compassY = (screenH + 16) / 2;
|
|
||||||
|
|
||||||
// Draw compass circle
|
|
||||||
display.drawCircle(compassX, compassY, compassRadius);
|
|
||||||
|
|
||||||
// Draw cardinal directions
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
|
||||||
display.drawString(compassX, compassY - compassRadius - 10, "N");
|
|
||||||
|
|
||||||
// Calculate relative bearing (bearing - heading)
|
|
||||||
const relativeBearing =
|
|
||||||
((compassInfo.bearing - compassInfo.heading) * Math.PI) / 180;
|
|
||||||
|
|
||||||
// Draw bearing arrow
|
|
||||||
const arrowLength = compassRadius - 6;
|
|
||||||
const arrowX = compassX + Math.sin(relativeBearing) * arrowLength;
|
|
||||||
const arrowY = compassY - Math.cos(relativeBearing) * arrowLength;
|
|
||||||
|
|
||||||
display.drawLine(compassX, compassY, arrowX, arrowY);
|
|
||||||
|
|
||||||
// Draw arrowhead
|
|
||||||
const headAngle = 0.4;
|
|
||||||
const headLength = 8;
|
|
||||||
const angle1 = relativeBearing + Math.PI - headAngle;
|
|
||||||
const angle2 = relativeBearing + Math.PI + headAngle;
|
|
||||||
display.drawLine(
|
|
||||||
arrowX,
|
|
||||||
arrowY,
|
|
||||||
arrowX + Math.sin(angle1) * headLength,
|
|
||||||
arrowY - Math.cos(angle1) * headLength,
|
|
||||||
);
|
|
||||||
display.drawLine(
|
|
||||||
arrowX,
|
|
||||||
arrowY,
|
|
||||||
arrowX + Math.sin(angle2) * headLength,
|
|
||||||
arrowY - Math.cos(angle2) * headLength,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Draw distance at bottom
|
|
||||||
if (compassInfo.distance !== undefined) {
|
|
||||||
let distStr: string;
|
|
||||||
if (compassInfo.distance >= 1000) {
|
|
||||||
distStr = `${(compassInfo.distance / 1000).toFixed(1)} km`;
|
|
||||||
} else {
|
|
||||||
distStr = `${Math.round(compassInfo.distance)} m`;
|
|
||||||
}
|
|
||||||
display.drawString(compassX, screenH - FONT_HEIGHT_SMALL, distStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a progress/loading screen
|
|
||||||
*/
|
|
||||||
export function drawProgressScreen(
|
|
||||||
display: OLEDDisplay,
|
|
||||||
title: string,
|
|
||||||
progress: number,
|
|
||||||
statusText?: string,
|
|
||||||
): void {
|
|
||||||
display.clear();
|
|
||||||
display.setFont(Font_6x8);
|
|
||||||
display.setColor(WHITE);
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
|
||||||
|
|
||||||
const screenW = display.getWidth();
|
|
||||||
const screenH = display.getHeight();
|
|
||||||
const centerX = screenW / 2;
|
|
||||||
const centerY = screenH / 2;
|
|
||||||
|
|
||||||
// Title
|
|
||||||
display.drawString(centerX, centerY - 20, title);
|
|
||||||
|
|
||||||
// Progress bar
|
|
||||||
const barWidth = screenW - 20;
|
|
||||||
const barHeight = 10;
|
|
||||||
const barX = 10;
|
|
||||||
const barY = centerY - barHeight / 2;
|
|
||||||
|
|
||||||
display.drawProgressBar(barX, barY, barWidth, barHeight, progress);
|
|
||||||
|
|
||||||
// Progress percentage
|
|
||||||
display.drawString(centerX, barY + barHeight + 4, `${Math.round(progress)}%`);
|
|
||||||
|
|
||||||
// Status text
|
|
||||||
if (statusText) {
|
|
||||||
display.drawString(centerX, screenH - FONT_HEIGHT_SMALL - 2, statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
}
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
/**
|
|
||||||
* SimpleFonts.ts - Simple working bitmap fonts for OLED emulator
|
|
||||||
*
|
|
||||||
* These are basic monospace bitmap fonts that are easy to render correctly.
|
|
||||||
* Format: Each character is stored in column-major order, 1 byte per column.
|
|
||||||
* For fonts taller than 8px, multiple bytes per column are used.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { OLEDFont } from "./OLEDDisplay";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple 6x8 monospace font
|
|
||||||
* Each character is 6 columns wide, 8 rows tall
|
|
||||||
* 1 byte per column (8 vertical pixels)
|
|
||||||
* Includes ASCII 32-126
|
|
||||||
*/
|
|
||||||
export const Font_6x8: OLEDFont = {
|
|
||||||
height: 8,
|
|
||||||
firstChar: 32,
|
|
||||||
charCount: 95,
|
|
||||||
widths: new Array(95).fill(6), // Fixed width font
|
|
||||||
data: new Uint8Array([
|
|
||||||
// Space (32)
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ! (33)
|
|
||||||
0x00, 0x00, 0x5f, 0x00, 0x00, 0x00,
|
|
||||||
// " (34)
|
|
||||||
0x00, 0x07, 0x00, 0x07, 0x00, 0x00,
|
|
||||||
// # (35)
|
|
||||||
0x14, 0x7f, 0x14, 0x7f, 0x14, 0x00,
|
|
||||||
// $ (36)
|
|
||||||
0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x00,
|
|
||||||
// % (37)
|
|
||||||
0x23, 0x13, 0x08, 0x64, 0x62, 0x00,
|
|
||||||
// & (38)
|
|
||||||
0x36, 0x49, 0x55, 0x22, 0x50, 0x00,
|
|
||||||
// ' (39)
|
|
||||||
0x00, 0x05, 0x03, 0x00, 0x00, 0x00,
|
|
||||||
// ( (40)
|
|
||||||
0x00, 0x1c, 0x22, 0x41, 0x00, 0x00,
|
|
||||||
// ) (41)
|
|
||||||
0x00, 0x41, 0x22, 0x1c, 0x00, 0x00,
|
|
||||||
// * (42)
|
|
||||||
0x08, 0x2a, 0x1c, 0x2a, 0x08, 0x00,
|
|
||||||
// + (43)
|
|
||||||
0x08, 0x08, 0x3e, 0x08, 0x08, 0x00,
|
|
||||||
// , (44)
|
|
||||||
0x00, 0x50, 0x30, 0x00, 0x00, 0x00,
|
|
||||||
// - (45)
|
|
||||||
0x08, 0x08, 0x08, 0x08, 0x08, 0x00,
|
|
||||||
// . (46)
|
|
||||||
0x00, 0x60, 0x60, 0x00, 0x00, 0x00,
|
|
||||||
// / (47)
|
|
||||||
0x20, 0x10, 0x08, 0x04, 0x02, 0x00,
|
|
||||||
// 0 (48)
|
|
||||||
0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00,
|
|
||||||
// 1 (49)
|
|
||||||
0x00, 0x42, 0x7f, 0x40, 0x00, 0x00,
|
|
||||||
// 2 (50)
|
|
||||||
0x42, 0x61, 0x51, 0x49, 0x46, 0x00,
|
|
||||||
// 3 (51)
|
|
||||||
0x21, 0x41, 0x45, 0x4b, 0x31, 0x00,
|
|
||||||
// 4 (52)
|
|
||||||
0x18, 0x14, 0x12, 0x7f, 0x10, 0x00,
|
|
||||||
// 5 (53)
|
|
||||||
0x27, 0x45, 0x45, 0x45, 0x39, 0x00,
|
|
||||||
// 6 (54)
|
|
||||||
0x3c, 0x4a, 0x49, 0x49, 0x30, 0x00,
|
|
||||||
// 7 (55)
|
|
||||||
0x01, 0x71, 0x09, 0x05, 0x03, 0x00,
|
|
||||||
// 8 (56)
|
|
||||||
0x36, 0x49, 0x49, 0x49, 0x36, 0x00,
|
|
||||||
// 9 (57)
|
|
||||||
0x06, 0x49, 0x49, 0x29, 0x1e, 0x00,
|
|
||||||
// : (58)
|
|
||||||
0x00, 0x36, 0x36, 0x00, 0x00, 0x00,
|
|
||||||
// ; (59)
|
|
||||||
0x00, 0x56, 0x36, 0x00, 0x00, 0x00,
|
|
||||||
// < (60)
|
|
||||||
0x00, 0x08, 0x14, 0x22, 0x41, 0x00,
|
|
||||||
// = (61)
|
|
||||||
0x14, 0x14, 0x14, 0x14, 0x14, 0x00,
|
|
||||||
// > (62)
|
|
||||||
0x41, 0x22, 0x14, 0x08, 0x00, 0x00,
|
|
||||||
// ? (63)
|
|
||||||
0x02, 0x01, 0x51, 0x09, 0x06, 0x00,
|
|
||||||
// @ (64)
|
|
||||||
0x32, 0x49, 0x79, 0x41, 0x3e, 0x00,
|
|
||||||
// A (65)
|
|
||||||
0x7e, 0x11, 0x11, 0x11, 0x7e, 0x00,
|
|
||||||
// B (66)
|
|
||||||
0x7f, 0x49, 0x49, 0x49, 0x36, 0x00,
|
|
||||||
// C (67)
|
|
||||||
0x3e, 0x41, 0x41, 0x41, 0x22, 0x00,
|
|
||||||
// D (68)
|
|
||||||
0x7f, 0x41, 0x41, 0x22, 0x1c, 0x00,
|
|
||||||
// E (69)
|
|
||||||
0x7f, 0x49, 0x49, 0x49, 0x41, 0x00,
|
|
||||||
// F (70)
|
|
||||||
0x7f, 0x09, 0x09, 0x01, 0x01, 0x00,
|
|
||||||
// G (71)
|
|
||||||
0x3e, 0x41, 0x41, 0x51, 0x32, 0x00,
|
|
||||||
// H (72)
|
|
||||||
0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00,
|
|
||||||
// I (73)
|
|
||||||
0x00, 0x41, 0x7f, 0x41, 0x00, 0x00,
|
|
||||||
// J (74)
|
|
||||||
0x20, 0x40, 0x41, 0x3f, 0x01, 0x00,
|
|
||||||
// K (75)
|
|
||||||
0x7f, 0x08, 0x14, 0x22, 0x41, 0x00,
|
|
||||||
// L (76)
|
|
||||||
0x7f, 0x40, 0x40, 0x40, 0x40, 0x00,
|
|
||||||
// M (77)
|
|
||||||
0x7f, 0x02, 0x04, 0x02, 0x7f, 0x00,
|
|
||||||
// N (78)
|
|
||||||
0x7f, 0x04, 0x08, 0x10, 0x7f, 0x00,
|
|
||||||
// O (79)
|
|
||||||
0x3e, 0x41, 0x41, 0x41, 0x3e, 0x00,
|
|
||||||
// P (80)
|
|
||||||
0x7f, 0x09, 0x09, 0x09, 0x06, 0x00,
|
|
||||||
// Q (81)
|
|
||||||
0x3e, 0x41, 0x51, 0x21, 0x5e, 0x00,
|
|
||||||
// R (82)
|
|
||||||
0x7f, 0x09, 0x19, 0x29, 0x46, 0x00,
|
|
||||||
// S (83)
|
|
||||||
0x46, 0x49, 0x49, 0x49, 0x31, 0x00,
|
|
||||||
// T (84)
|
|
||||||
0x01, 0x01, 0x7f, 0x01, 0x01, 0x00,
|
|
||||||
// U (85)
|
|
||||||
0x3f, 0x40, 0x40, 0x40, 0x3f, 0x00,
|
|
||||||
// V (86)
|
|
||||||
0x1f, 0x20, 0x40, 0x20, 0x1f, 0x00,
|
|
||||||
// W (87)
|
|
||||||
0x7f, 0x20, 0x18, 0x20, 0x7f, 0x00,
|
|
||||||
// X (88)
|
|
||||||
0x63, 0x14, 0x08, 0x14, 0x63, 0x00,
|
|
||||||
// Y (89)
|
|
||||||
0x03, 0x04, 0x78, 0x04, 0x03, 0x00,
|
|
||||||
// Z (90)
|
|
||||||
0x61, 0x51, 0x49, 0x45, 0x43, 0x00,
|
|
||||||
// [ (91)
|
|
||||||
0x00, 0x00, 0x7f, 0x41, 0x41, 0x00,
|
|
||||||
// \ (92)
|
|
||||||
0x02, 0x04, 0x08, 0x10, 0x20, 0x00,
|
|
||||||
// ] (93)
|
|
||||||
0x41, 0x41, 0x7f, 0x00, 0x00, 0x00,
|
|
||||||
// ^ (94)
|
|
||||||
0x04, 0x02, 0x01, 0x02, 0x04, 0x00,
|
|
||||||
// _ (95)
|
|
||||||
0x40, 0x40, 0x40, 0x40, 0x40, 0x00,
|
|
||||||
// ` (96)
|
|
||||||
0x00, 0x01, 0x02, 0x04, 0x00, 0x00,
|
|
||||||
// a (97)
|
|
||||||
0x20, 0x54, 0x54, 0x54, 0x78, 0x00,
|
|
||||||
// b (98)
|
|
||||||
0x7f, 0x48, 0x44, 0x44, 0x38, 0x00,
|
|
||||||
// c (99)
|
|
||||||
0x38, 0x44, 0x44, 0x44, 0x20, 0x00,
|
|
||||||
// d (100)
|
|
||||||
0x38, 0x44, 0x44, 0x48, 0x7f, 0x00,
|
|
||||||
// e (101)
|
|
||||||
0x38, 0x54, 0x54, 0x54, 0x18, 0x00,
|
|
||||||
// f (102)
|
|
||||||
0x08, 0x7e, 0x09, 0x01, 0x02, 0x00,
|
|
||||||
// g (103)
|
|
||||||
0x08, 0x14, 0x54, 0x54, 0x3c, 0x00,
|
|
||||||
// h (104)
|
|
||||||
0x7f, 0x08, 0x04, 0x04, 0x78, 0x00,
|
|
||||||
// i (105)
|
|
||||||
0x00, 0x44, 0x7d, 0x40, 0x00, 0x00,
|
|
||||||
// j (106)
|
|
||||||
0x20, 0x40, 0x44, 0x3d, 0x00, 0x00,
|
|
||||||
// k (107)
|
|
||||||
0x00, 0x7f, 0x10, 0x28, 0x44, 0x00,
|
|
||||||
// l (108)
|
|
||||||
0x00, 0x41, 0x7f, 0x40, 0x00, 0x00,
|
|
||||||
// m (109)
|
|
||||||
0x7c, 0x04, 0x18, 0x04, 0x78, 0x00,
|
|
||||||
// n (110)
|
|
||||||
0x7c, 0x08, 0x04, 0x04, 0x78, 0x00,
|
|
||||||
// o (111)
|
|
||||||
0x38, 0x44, 0x44, 0x44, 0x38, 0x00,
|
|
||||||
// p (112)
|
|
||||||
0x7c, 0x14, 0x14, 0x14, 0x08, 0x00,
|
|
||||||
// q (113)
|
|
||||||
0x08, 0x14, 0x14, 0x18, 0x7c, 0x00,
|
|
||||||
// r (114)
|
|
||||||
0x7c, 0x08, 0x04, 0x04, 0x08, 0x00,
|
|
||||||
// s (115)
|
|
||||||
0x48, 0x54, 0x54, 0x54, 0x20, 0x00,
|
|
||||||
// t (116)
|
|
||||||
0x04, 0x3f, 0x44, 0x40, 0x20, 0x00,
|
|
||||||
// u (117)
|
|
||||||
0x3c, 0x40, 0x40, 0x20, 0x7c, 0x00,
|
|
||||||
// v (118)
|
|
||||||
0x1c, 0x20, 0x40, 0x20, 0x1c, 0x00,
|
|
||||||
// w (119)
|
|
||||||
0x3c, 0x40, 0x30, 0x40, 0x3c, 0x00,
|
|
||||||
// x (120)
|
|
||||||
0x44, 0x28, 0x10, 0x28, 0x44, 0x00,
|
|
||||||
// y (121)
|
|
||||||
0x0c, 0x50, 0x50, 0x50, 0x3c, 0x00,
|
|
||||||
// z (122)
|
|
||||||
0x44, 0x64, 0x54, 0x4c, 0x44, 0x00,
|
|
||||||
// { (123)
|
|
||||||
0x00, 0x08, 0x36, 0x41, 0x00, 0x00,
|
|
||||||
// | (124)
|
|
||||||
0x00, 0x00, 0x7f, 0x00, 0x00, 0x00,
|
|
||||||
// } (125)
|
|
||||||
0x00, 0x41, 0x36, 0x08, 0x00, 0x00,
|
|
||||||
// ~ (126)
|
|
||||||
0x08, 0x08, 0x2a, 0x1c, 0x08, 0x00,
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple 5x7 monospace font (more compact)
|
|
||||||
* Each character is 5 columns wide, 7 rows tall
|
|
||||||
* 1 byte per column
|
|
||||||
*/
|
|
||||||
export const Font_5x7: OLEDFont = {
|
|
||||||
height: 7,
|
|
||||||
firstChar: 32,
|
|
||||||
charCount: 95,
|
|
||||||
widths: new Array(95).fill(5),
|
|
||||||
data: new Uint8Array([
|
|
||||||
// Space (32)
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
// ! (33)
|
|
||||||
0x00, 0x00, 0x2f, 0x00, 0x00,
|
|
||||||
// " (34)
|
|
||||||
0x00, 0x07, 0x00, 0x07, 0x00,
|
|
||||||
// # (35)
|
|
||||||
0x14, 0x3e, 0x14, 0x3e, 0x14,
|
|
||||||
// $ (36)
|
|
||||||
0x24, 0x2a, 0x7f, 0x2a, 0x12,
|
|
||||||
// % (37)
|
|
||||||
0x23, 0x13, 0x08, 0x64, 0x62,
|
|
||||||
// & (38)
|
|
||||||
0x36, 0x49, 0x55, 0x22, 0x50,
|
|
||||||
// ' (39)
|
|
||||||
0x00, 0x05, 0x03, 0x00, 0x00,
|
|
||||||
// ( (40)
|
|
||||||
0x00, 0x1c, 0x22, 0x41, 0x00,
|
|
||||||
// ) (41)
|
|
||||||
0x00, 0x41, 0x22, 0x1c, 0x00,
|
|
||||||
// * (42)
|
|
||||||
0x14, 0x08, 0x3e, 0x08, 0x14,
|
|
||||||
// + (43)
|
|
||||||
0x08, 0x08, 0x3e, 0x08, 0x08,
|
|
||||||
// , (44)
|
|
||||||
0x00, 0x50, 0x30, 0x00, 0x00,
|
|
||||||
// - (45)
|
|
||||||
0x08, 0x08, 0x08, 0x08, 0x08,
|
|
||||||
// . (46)
|
|
||||||
0x00, 0x30, 0x30, 0x00, 0x00,
|
|
||||||
// / (47)
|
|
||||||
0x20, 0x10, 0x08, 0x04, 0x02,
|
|
||||||
// 0 (48)
|
|
||||||
0x3e, 0x51, 0x49, 0x45, 0x3e,
|
|
||||||
// 1 (49)
|
|
||||||
0x00, 0x42, 0x7f, 0x40, 0x00,
|
|
||||||
// 2 (50)
|
|
||||||
0x42, 0x61, 0x51, 0x49, 0x46,
|
|
||||||
// 3 (51)
|
|
||||||
0x21, 0x41, 0x45, 0x4b, 0x31,
|
|
||||||
// 4 (52)
|
|
||||||
0x18, 0x14, 0x12, 0x7f, 0x10,
|
|
||||||
// 5 (53)
|
|
||||||
0x27, 0x45, 0x45, 0x45, 0x39,
|
|
||||||
// 6 (54)
|
|
||||||
0x3c, 0x4a, 0x49, 0x49, 0x30,
|
|
||||||
// 7 (55)
|
|
||||||
0x01, 0x71, 0x09, 0x05, 0x03,
|
|
||||||
// 8 (56)
|
|
||||||
0x36, 0x49, 0x49, 0x49, 0x36,
|
|
||||||
// 9 (57)
|
|
||||||
0x06, 0x49, 0x49, 0x29, 0x1e,
|
|
||||||
// : (58)
|
|
||||||
0x00, 0x36, 0x36, 0x00, 0x00,
|
|
||||||
// ; (59)
|
|
||||||
0x00, 0x56, 0x36, 0x00, 0x00,
|
|
||||||
// < (60)
|
|
||||||
0x08, 0x14, 0x22, 0x41, 0x00,
|
|
||||||
// = (61)
|
|
||||||
0x14, 0x14, 0x14, 0x14, 0x14,
|
|
||||||
// > (62)
|
|
||||||
0x00, 0x41, 0x22, 0x14, 0x08,
|
|
||||||
// ? (63)
|
|
||||||
0x02, 0x01, 0x51, 0x09, 0x06,
|
|
||||||
// @ (64)
|
|
||||||
0x32, 0x49, 0x79, 0x41, 0x3e,
|
|
||||||
// A (65)
|
|
||||||
0x7e, 0x11, 0x11, 0x11, 0x7e,
|
|
||||||
// B (66)
|
|
||||||
0x7f, 0x49, 0x49, 0x49, 0x36,
|
|
||||||
// C (67)
|
|
||||||
0x3e, 0x41, 0x41, 0x41, 0x22,
|
|
||||||
// D (68)
|
|
||||||
0x7f, 0x41, 0x41, 0x22, 0x1c,
|
|
||||||
// E (69)
|
|
||||||
0x7f, 0x49, 0x49, 0x49, 0x41,
|
|
||||||
// F (70)
|
|
||||||
0x7f, 0x09, 0x09, 0x09, 0x01,
|
|
||||||
// G (71)
|
|
||||||
0x3e, 0x41, 0x49, 0x49, 0x7a,
|
|
||||||
// H (72)
|
|
||||||
0x7f, 0x08, 0x08, 0x08, 0x7f,
|
|
||||||
// I (73)
|
|
||||||
0x00, 0x41, 0x7f, 0x41, 0x00,
|
|
||||||
// J (74)
|
|
||||||
0x20, 0x40, 0x41, 0x3f, 0x01,
|
|
||||||
// K (75)
|
|
||||||
0x7f, 0x08, 0x14, 0x22, 0x41,
|
|
||||||
// L (76)
|
|
||||||
0x7f, 0x40, 0x40, 0x40, 0x40,
|
|
||||||
// M (77)
|
|
||||||
0x7f, 0x02, 0x0c, 0x02, 0x7f,
|
|
||||||
// N (78)
|
|
||||||
0x7f, 0x04, 0x08, 0x10, 0x7f,
|
|
||||||
// O (79)
|
|
||||||
0x3e, 0x41, 0x41, 0x41, 0x3e,
|
|
||||||
// P (80)
|
|
||||||
0x7f, 0x09, 0x09, 0x09, 0x06,
|
|
||||||
// Q (81)
|
|
||||||
0x3e, 0x41, 0x51, 0x21, 0x5e,
|
|
||||||
// R (82)
|
|
||||||
0x7f, 0x09, 0x19, 0x29, 0x46,
|
|
||||||
// S (83)
|
|
||||||
0x46, 0x49, 0x49, 0x49, 0x31,
|
|
||||||
// T (84)
|
|
||||||
0x01, 0x01, 0x7f, 0x01, 0x01,
|
|
||||||
// U (85)
|
|
||||||
0x3f, 0x40, 0x40, 0x40, 0x3f,
|
|
||||||
// V (86)
|
|
||||||
0x1f, 0x20, 0x40, 0x20, 0x1f,
|
|
||||||
// W (87)
|
|
||||||
0x3f, 0x40, 0x38, 0x40, 0x3f,
|
|
||||||
// X (88)
|
|
||||||
0x63, 0x14, 0x08, 0x14, 0x63,
|
|
||||||
// Y (89)
|
|
||||||
0x07, 0x08, 0x70, 0x08, 0x07,
|
|
||||||
// Z (90)
|
|
||||||
0x61, 0x51, 0x49, 0x45, 0x43,
|
|
||||||
// [ (91)
|
|
||||||
0x00, 0x7f, 0x41, 0x41, 0x00,
|
|
||||||
// \ (92)
|
|
||||||
0x02, 0x04, 0x08, 0x10, 0x20,
|
|
||||||
// ] (93)
|
|
||||||
0x00, 0x41, 0x41, 0x7f, 0x00,
|
|
||||||
// ^ (94)
|
|
||||||
0x04, 0x02, 0x01, 0x02, 0x04,
|
|
||||||
// _ (95)
|
|
||||||
0x40, 0x40, 0x40, 0x40, 0x40,
|
|
||||||
// ` (96)
|
|
||||||
0x00, 0x01, 0x02, 0x04, 0x00,
|
|
||||||
// a (97)
|
|
||||||
0x20, 0x54, 0x54, 0x54, 0x78,
|
|
||||||
// b (98)
|
|
||||||
0x7f, 0x48, 0x44, 0x44, 0x38,
|
|
||||||
// c (99)
|
|
||||||
0x38, 0x44, 0x44, 0x44, 0x20,
|
|
||||||
// d (100)
|
|
||||||
0x38, 0x44, 0x44, 0x48, 0x7f,
|
|
||||||
// e (101)
|
|
||||||
0x38, 0x54, 0x54, 0x54, 0x18,
|
|
||||||
// f (102)
|
|
||||||
0x08, 0x7e, 0x09, 0x01, 0x02,
|
|
||||||
// g (103)
|
|
||||||
0x0c, 0x52, 0x52, 0x52, 0x3e,
|
|
||||||
// h (104)
|
|
||||||
0x7f, 0x08, 0x04, 0x04, 0x78,
|
|
||||||
// i (105)
|
|
||||||
0x00, 0x44, 0x7d, 0x40, 0x00,
|
|
||||||
// j (106)
|
|
||||||
0x20, 0x40, 0x44, 0x3d, 0x00,
|
|
||||||
// k (107)
|
|
||||||
0x7f, 0x10, 0x28, 0x44, 0x00,
|
|
||||||
// l (108)
|
|
||||||
0x00, 0x41, 0x7f, 0x40, 0x00,
|
|
||||||
// m (109)
|
|
||||||
0x7c, 0x04, 0x18, 0x04, 0x78,
|
|
||||||
// n (110)
|
|
||||||
0x7c, 0x08, 0x04, 0x04, 0x78,
|
|
||||||
// o (111)
|
|
||||||
0x38, 0x44, 0x44, 0x44, 0x38,
|
|
||||||
// p (112)
|
|
||||||
0x7c, 0x14, 0x14, 0x14, 0x08,
|
|
||||||
// q (113)
|
|
||||||
0x08, 0x14, 0x14, 0x18, 0x7c,
|
|
||||||
// r (114)
|
|
||||||
0x7c, 0x08, 0x04, 0x04, 0x08,
|
|
||||||
// s (115)
|
|
||||||
0x48, 0x54, 0x54, 0x54, 0x20,
|
|
||||||
// t (116)
|
|
||||||
0x04, 0x3f, 0x44, 0x40, 0x20,
|
|
||||||
// u (117)
|
|
||||||
0x3c, 0x40, 0x40, 0x20, 0x7c,
|
|
||||||
// v (118)
|
|
||||||
0x1c, 0x20, 0x40, 0x20, 0x1c,
|
|
||||||
// w (119)
|
|
||||||
0x3c, 0x40, 0x30, 0x40, 0x3c,
|
|
||||||
// x (120)
|
|
||||||
0x44, 0x28, 0x10, 0x28, 0x44,
|
|
||||||
// y (121)
|
|
||||||
0x0c, 0x50, 0x50, 0x50, 0x3c,
|
|
||||||
// z (122)
|
|
||||||
0x44, 0x64, 0x54, 0x4c, 0x44,
|
|
||||||
// { (123)
|
|
||||||
0x00, 0x08, 0x36, 0x41, 0x00,
|
|
||||||
// | (124)
|
|
||||||
0x00, 0x00, 0x7f, 0x00, 0x00,
|
|
||||||
// } (125)
|
|
||||||
0x00, 0x41, 0x36, 0x08, 0x00,
|
|
||||||
// ~ (126)
|
|
||||||
0x10, 0x08, 0x08, 0x10, 0x08,
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Alias for compatibility
|
|
||||||
export const SimpleFont = Font_6x8;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
/**
|
|
||||||
* Meshtastic OLED Emulator
|
|
||||||
*
|
|
||||||
* A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays.
|
|
||||||
* Provides the same OLEDDisplay API as the Meshtastic firmware, enabling
|
|
||||||
* screen development and testing in the browser.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Core display class
|
|
||||||
export {
|
|
||||||
OLEDDisplay,
|
|
||||||
type OLEDDISPLAY_GEOMETRY,
|
|
||||||
type OLEDDISPLAY_COLOR,
|
|
||||||
type OLEDDISPLAY_TEXT_ALIGNMENT,
|
|
||||||
type OLEDFont,
|
|
||||||
} from "./OLEDDisplay";
|
|
||||||
export {
|
|
||||||
WHITE,
|
|
||||||
BLACK,
|
|
||||||
INVERSE,
|
|
||||||
TEXT_ALIGN_LEFT,
|
|
||||||
TEXT_ALIGN_CENTER,
|
|
||||||
TEXT_ALIGN_RIGHT,
|
|
||||||
} from "./OLEDDisplay";
|
|
||||||
|
|
||||||
// Fonts
|
|
||||||
export {
|
|
||||||
ArialMT_Plain_10,
|
|
||||||
ArialMT_Plain_16,
|
|
||||||
FONT_HEIGHT_SMALL,
|
|
||||||
FONT_HEIGHT_MEDIUM,
|
|
||||||
FONT_HEIGHT_LARGE,
|
|
||||||
createFontFromBytes,
|
|
||||||
} from "./OLEDFonts";
|
|
||||||
|
|
||||||
// Simple working fonts (recommended)
|
|
||||||
export { Font_6x8, Font_5x7, SimpleFont } from "./SimpleFonts";
|
|
||||||
|
|
||||||
// Images and icons
|
|
||||||
export * as OLEDImages from "./OLEDImages";
|
|
||||||
|
|
||||||
// Screen renderers (firmware UI ports)
|
|
||||||
export * as ScreenRenderers from "./ScreenRenderers";
|
|
||||||
|
|
||||||
// Vue component
|
|
||||||
export { default as OLEDEmulator } from "./OLEDEmulator.vue";
|
|
||||||
export { DISPLAY_PRESETS, type DisplayPreset } from "./DisplayPresets";
|
|
||||||
|
|
||||||
// Demo component
|
|
||||||
export { default as OLEDEmulatorDemo } from "./Demo.vue";
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { createApp } from "vue";
|
|
||||||
import Demo from "./Demo.vue";
|
|
||||||
|
|
||||||
createApp(Demo).mount("#app");
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
|
|
||||||
/* Vue */
|
|
||||||
"types": ["vite/client"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { defineConfig } from "vite";
|
|
||||||
import vue from "@vitejs/plugin-vue";
|
|
||||||
import dts from "vite-plugin-dts";
|
|
||||||
import { resolve } from "path";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
vue(),
|
|
||||||
dts({
|
|
||||||
insertTypesEntry: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: resolve(__dirname, "src/index.ts"),
|
|
||||||
name: "MeshtasticOLEDEmulator",
|
|
||||||
formats: ["es", "cjs"],
|
|
||||||
fileName: (format) => `index.${format === "es" ? "mjs" : "js"}`,
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: ["vue"],
|
|
||||||
output: {
|
|
||||||
globals: {
|
|
||||||
vue: "Vue",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cssCodeSplit: false,
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"@": resolve(__dirname, "src"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+20
-49
@@ -50,15 +50,11 @@ build_flags = -Wno-missing-field-initializers
|
|||||||
-DRADIOLIB_EXCLUDE_APRS=1
|
-DRADIOLIB_EXCLUDE_APRS=1
|
||||||
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
||||||
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
||||||
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
|
|
||||||
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
||||||
-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
|
|
||||||
-DMESHTASTIC_EXCLUDE_STATUS=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
|
||||||
@@ -68,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
|
||||||
@@ -107,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]
|
||||||
@@ -132,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
|
||||||
@@ -145,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.9
|
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
|
||||||
@@ -159,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
|
||||||
@@ -168,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
|
||||||
@@ -212,38 +215,6 @@ lib_deps =
|
|||||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
# 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
|
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||||
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
||||||
sensirion/Sensirion Core@0.7.3
|
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
|
||||||
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
|
||||||
sensirion/Sensirion I2C SFA3x@1.0.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.3
|
|
||||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
|
||||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
|
||||||
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
|
||||||
sensirion/Sensirion I2C SFA3x@1.0.0
|
|
||||||
|
|||||||
+1
-1
Submodule protobufs updated: 44298d374f...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
|
|
||||||
+59
-66
@@ -1,21 +1,19 @@
|
|||||||
#ifndef AMBIENTLIGHTINGTHREAD_H
|
|
||||||
#define AMBIENTLIGHTINGTHREAD_H
|
|
||||||
|
|
||||||
#include "Observer.h"
|
#include "Observer.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "detect/ScanI2C.h"
|
|
||||||
#include "sleep.h"
|
|
||||||
|
|
||||||
#ifdef HAS_NCP5623
|
#ifdef HAS_NCP5623
|
||||||
#include <NCP5623.h>
|
#include <graphics/RAKled.h>
|
||||||
|
NCP5623 rgb;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAS_LP5562
|
#ifdef HAS_LP5562
|
||||||
#include <graphics/NomadStarLED.h>
|
#include <graphics/NomadStarLED.h>
|
||||||
|
LP5562 rgbw;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAS_NEOPIXEL
|
#ifdef HAS_NEOPIXEL
|
||||||
#include <Adafruit_NeoPixel.h>
|
#include <graphics/NeoPixel.h>
|
||||||
|
Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef UNPHONE
|
#ifdef UNPHONE
|
||||||
@@ -23,24 +21,10 @@
|
|||||||
extern unPhone unphone;
|
extern unPhone unphone;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace concurrency
|
||||||
|
{
|
||||||
class AmbientLightingThread : public concurrency::OSThread
|
class AmbientLightingThread : public concurrency::OSThread
|
||||||
{
|
{
|
||||||
friend class StatusLEDModule; // Let the LEDStatusModule trigger the ambient lighting for notifications and battery status.
|
|
||||||
friend class ExternalNotificationModule; // Let the ExternalNotificationModule trigger the ambient lighting for notifications.
|
|
||||||
|
|
||||||
private:
|
|
||||||
#ifdef HAS_NCP5623
|
|
||||||
NCP5623 rgb;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef HAS_LP5562
|
|
||||||
LP5562 rgbw;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef HAS_NEOPIXEL
|
|
||||||
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting")
|
explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting")
|
||||||
{
|
{
|
||||||
@@ -52,15 +36,14 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
moduleConfig.ambient_lighting.led_state = true;
|
moduleConfig.ambient_lighting.led_state = true;
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#if AMBIENT_LIGHTING_TEST
|
// Uncomment to test module
|
||||||
// define to enable test
|
// moduleConfig.ambient_lighting.led_state = true;
|
||||||
moduleConfig.ambient_lighting.led_state = true;
|
// moduleConfig.ambient_lighting.current = 10;
|
||||||
moduleConfig.ambient_lighting.current = 10;
|
|
||||||
// Default to a color based on our node number
|
// Default to a color based on our node number
|
||||||
moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
// moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
||||||
moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
// moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
||||||
moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
// moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
||||||
#endif
|
|
||||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||||
_type = type;
|
_type = type;
|
||||||
if (_type == ScanI2C::DeviceType::NONE) {
|
if (_type == ScanI2C::DeviceType::NONE) {
|
||||||
@@ -70,6 +53,11 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAS_RGB_LED
|
#ifdef HAS_RGB_LED
|
||||||
|
if (!moduleConfig.ambient_lighting.led_state) {
|
||||||
|
LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF");
|
||||||
|
disable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
LOG_DEBUG("AmbientLighting init");
|
LOG_DEBUG("AmbientLighting init");
|
||||||
#ifdef HAS_NCP5623
|
#ifdef HAS_NCP5623
|
||||||
if (_type == ScanI2C::NCP5623) {
|
if (_type == ScanI2C::NCP5623) {
|
||||||
@@ -89,13 +77,7 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
pixels.clear(); // Set all pixel colors to 'off'
|
pixels.clear(); // Set all pixel colors to 'off'
|
||||||
pixels.setBrightness(moduleConfig.ambient_lighting.current);
|
pixels.setBrightness(moduleConfig.ambient_lighting.current);
|
||||||
#endif
|
#endif
|
||||||
if (!moduleConfig.ambient_lighting.led_state) {
|
setLighting();
|
||||||
LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF");
|
|
||||||
disable();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
|
||||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
|
||||||
#endif
|
#endif
|
||||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||||
}
|
}
|
||||||
@@ -109,8 +91,7 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||||
if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) {
|
if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) {
|
||||||
#endif
|
#endif
|
||||||
setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
setLighting();
|
||||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
|
||||||
return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification
|
return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification
|
||||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||||
}
|
}
|
||||||
@@ -167,53 +148,65 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
void setLighting()
|
||||||
void setLighting(float current, uint8_t red, uint8_t green, uint8_t blue)
|
|
||||||
{
|
{
|
||||||
#ifdef HAS_NCP5623
|
#ifdef HAS_NCP5623
|
||||||
rgb.setCurrent(current);
|
rgb.setCurrent(moduleConfig.ambient_lighting.current);
|
||||||
rgb.setRed(red);
|
rgb.setRed(moduleConfig.ambient_lighting.red);
|
||||||
rgb.setGreen(green);
|
rgb.setGreen(moduleConfig.ambient_lighting.green);
|
||||||
rgb.setBlue(blue);
|
rgb.setBlue(moduleConfig.ambient_lighting.blue);
|
||||||
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
|
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d",
|
||||||
|
moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||||
|
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAS_LP5562
|
#ifdef HAS_LP5562
|
||||||
rgbw.setCurrent(current);
|
rgbw.setCurrent(moduleConfig.ambient_lighting.current);
|
||||||
rgbw.setRed(red);
|
rgbw.setRed(moduleConfig.ambient_lighting.red);
|
||||||
rgbw.setGreen(green);
|
rgbw.setGreen(moduleConfig.ambient_lighting.green);
|
||||||
rgbw.setBlue(blue);
|
rgbw.setBlue(moduleConfig.ambient_lighting.blue);
|
||||||
LOG_DEBUG("Init LP5562 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
|
LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current,
|
||||||
|
moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAS_NEOPIXEL
|
#ifdef HAS_NEOPIXEL
|
||||||
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
|
pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
|
||||||
|
moduleConfig.ambient_lighting.blue),
|
||||||
|
0, NEOPIXEL_COUNT);
|
||||||
|
|
||||||
// RadioMaster Bandit has addressable LED at the two buttons
|
// RadioMaster Bandit has addressable LED at the two buttons
|
||||||
// this allow us to set different lighting for them in variant.h file.
|
// this allow us to set different lighting for them in variant.h file.
|
||||||
|
#ifdef RADIOMASTER_900_BANDIT
|
||||||
#if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX)
|
#if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX)
|
||||||
pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1);
|
pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1);
|
||||||
#endif
|
#endif
|
||||||
#if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX)
|
#if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX)
|
||||||
pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1);
|
pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1);
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
pixels.show();
|
pixels.show();
|
||||||
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%f, red=%d, green=%d, blue=%d",
|
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
|
||||||
// current, red, green, blue);
|
// moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||||
|
// moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
#endif
|
#endif
|
||||||
#ifdef RGBLED_CA
|
#ifdef RGBLED_CA
|
||||||
analogWrite(RGBLED_RED, 255 - red);
|
analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);
|
||||||
analogWrite(RGBLED_GREEN, 255 - green);
|
analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green);
|
||||||
analogWrite(RGBLED_BLUE, 255 - blue);
|
analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue);
|
||||||
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", red, green, blue);
|
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
||||||
|
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
#elif defined(RGBLED_RED)
|
#elif defined(RGBLED_RED)
|
||||||
analogWrite(RGBLED_RED, red);
|
analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red);
|
||||||
analogWrite(RGBLED_GREEN, green);
|
analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green);
|
||||||
analogWrite(RGBLED_BLUE, blue);
|
analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue);
|
||||||
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", red, green, blue);
|
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
||||||
|
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
#endif
|
#endif
|
||||||
#ifdef UNPHONE
|
#ifdef UNPHONE
|
||||||
unphone.rgb(red, green, blue);
|
unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
|
||||||
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", red, green, blue);
|
moduleConfig.ambient_lighting.blue);
|
||||||
|
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
||||||
|
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
#endif // AMBIENTLIGHTINGTHREAD_H
|
|
||||||
|
} // namespace concurrency
|
||||||
|
|||||||
+10
-2
@@ -89,14 +89,22 @@ class BluetoothStatus : public Status
|
|||||||
case ConnectionState::CONNECTED:
|
case ConnectionState::CONNECTED:
|
||||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||||
#ifdef BLE_LED
|
#ifdef BLE_LED
|
||||||
digitalWrite(BLE_LED, LED_STATE_ON);
|
#ifdef BLE_LED_INVERTED
|
||||||
|
digitalWrite(BLE_LED, LOW);
|
||||||
|
#else
|
||||||
|
digitalWrite(BLE_LED, HIGH);
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ConnectionState::DISCONNECTED:
|
case ConnectionState::DISCONNECTED:
|
||||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||||
#ifdef BLE_LED
|
#ifdef BLE_LED
|
||||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
#ifdef BLE_LED_INVERTED
|
||||||
|
digitalWrite(BLE_LED, HIGH);
|
||||||
|
#else
|
||||||
|
digitalWrite(BLE_LED, LOW);
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -336,3 +339,62 @@ void setupSDCard()
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -56,3 +69,7 @@ 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);
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#include "Led.h"
|
||||||
|
#include "PowerMon.h"
|
||||||
|
#include "main.h"
|
||||||
|
#include "power.h"
|
||||||
|
|
||||||
|
GpioVirtPin ledForceOn, ledBlink;
|
||||||
|
|
||||||
|
#if defined(LED_PIN)
|
||||||
|
// Most boards have a GPIO for LED control
|
||||||
|
static GpioHwPin ledRawHwPin(LED_PIN);
|
||||||
|
#else
|
||||||
|
static GpioVirtPin ledRawHwPin; // Dummy pin for no hardware
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if LED_STATE_ON == 0
|
||||||
|
static GpioVirtPin ledHwPin;
|
||||||
|
static GpioNotTransformer ledInverter(&ledHwPin, &ledRawHwPin);
|
||||||
|
#else
|
||||||
|
static GpioPin &ledHwPin = ledRawHwPin;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(HAS_PMU)
|
||||||
|
/**
|
||||||
|
* A GPIO controlled by the PMU
|
||||||
|
*/
|
||||||
|
class GpioPmuPin : public GpioPin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void set(bool value)
|
||||||
|
{
|
||||||
|
if (pmu_found && PMU) {
|
||||||
|
// blink the axp led
|
||||||
|
PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ledPmuHwPin;
|
||||||
|
|
||||||
|
// In some cases we need to drive a PMU LED and a normal LED
|
||||||
|
static GpioSplitter ledFinalPin(&ledHwPin, &ledPmuHwPin);
|
||||||
|
#else
|
||||||
|
static GpioPin &ledFinalPin = ledHwPin;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_POWERMON
|
||||||
|
/**
|
||||||
|
* We monitor changes to the LED drive output because we use that as a sanity test in our power monitor stuff.
|
||||||
|
*/
|
||||||
|
class MonitoredLedPin : public GpioPin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void set(bool value)
|
||||||
|
{
|
||||||
|
if (powerMon) {
|
||||||
|
if (value)
|
||||||
|
powerMon->setState(meshtastic_PowerMon_State_LED_On);
|
||||||
|
else
|
||||||
|
powerMon->clearState(meshtastic_PowerMon_State_LED_On);
|
||||||
|
}
|
||||||
|
ledFinalPin.set(value);
|
||||||
|
}
|
||||||
|
} monitoredLedPin;
|
||||||
|
#else
|
||||||
|
static GpioPin &monitoredLedPin = ledFinalPin;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static GpioBinaryTransformer ledForcer(&ledForceOn, &ledBlink, &monitoredLedPin, GpioBinaryTransformer::Or);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include "GpioLogic.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ledForceOn and ledForceOff both override the normal ledBlinker behavior (which is controlled by main)
|
||||||
|
*/
|
||||||
|
extern GpioVirtPin ledForceOn, ledBlink;
|
||||||
@@ -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
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user