mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-04 11:28:11 +10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83889a1c37 |
@@ -20,7 +20,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
cmake git zip libgpiod-dev libbluetooth-dev libi2c-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/* && \
|
||||
pip install --no-cache-dir -U \
|
||||
platformio==6.1.16 \
|
||||
|
||||
@@ -29,6 +29,23 @@ jobs:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
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 }}
|
||||
id: build
|
||||
uses: meshtastic/gh-action-firmware@main
|
||||
@@ -36,66 +53,8 @@ jobs:
|
||||
pio_platform: ${{ inputs.platform }}
|
||||
pio_env: ${{ inputs.pio_env }}
|
||||
pio_target: build
|
||||
|
||||
- name: ESP32 - Download Unified OTA firmware
|
||||
# Currently only esp32 and esp32s3 use the unified ota
|
||||
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
|
||||
ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
|
||||
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
|
||||
|
||||
- name: Job summary
|
||||
env:
|
||||
|
||||
@@ -201,7 +201,6 @@ jobs:
|
||||
./device-*.bat
|
||||
./littlefs-*.bin
|
||||
./bleota*bin
|
||||
./mt-*-ota.bin
|
||||
./Meshtastic_nRF52_factory_erase*.uf2
|
||||
retention-days: 30
|
||||
|
||||
@@ -294,24 +293,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
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
|
||||
uses: softprops/action-gh-release@v2
|
||||
@@ -321,7 +302,8 @@ jobs:
|
||||
prerelease: true
|
||||
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
|
||||
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
|
||||
uses: actions/download-artifact@v7
|
||||
@@ -445,8 +427,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
@@ -466,13 +446,6 @@ jobs:
|
||||
pattern: manifest-${{ needs.version.outputs.long }}
|
||||
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
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
env:
|
||||
|
||||
@@ -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] });
|
||||
@@ -48,37 +48,6 @@ jobs:
|
||||
${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }}
|
||||
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
|
||||
bump-version:
|
||||
if: github.event.action == 'published'
|
||||
|
||||
+1
-4
@@ -48,8 +48,5 @@ arduino-lib-builder*
|
||||
dependencies.lock
|
||||
idf_component.yml
|
||||
CMakeLists.txt
|
||||
/sdkconfig.*
|
||||
sdkconfig.*
|
||||
.dummy/*
|
||||
|
||||
# PYTHONPATH used by the Nix shell
|
||||
.python3
|
||||
|
||||
+10
-10
@@ -8,25 +8,25 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.501
|
||||
- renovate@43.10.3
|
||||
- prettier@3.8.1
|
||||
- trufflehog@3.93.3
|
||||
- yamllint@1.38.0
|
||||
- bandit@1.9.3
|
||||
- trivy@0.69.1
|
||||
- checkov@3.2.497
|
||||
- renovate@42.75.0
|
||||
- prettier@3.7.4
|
||||
- trufflehog@3.92.4
|
||||
- yamllint@1.37.1
|
||||
- bandit@1.9.2
|
||||
- trivy@0.68.2
|
||||
- taplo@0.10.0
|
||||
- ruff@0.15.1
|
||||
- ruff@0.14.11
|
||||
- isort@7.0.0
|
||||
- markdownlint@0.47.0
|
||||
- oxipng@10.1.0
|
||||
- oxipng@10.0.0
|
||||
- svgo@4.0.0
|
||||
- actionlint@1.7.10
|
||||
- flake8@7.3.0
|
||||
- hadolint@2.14.0
|
||||
- shfmt@3.6.0
|
||||
- shellcheck@0.11.0
|
||||
- black@26.1.0
|
||||
- black@25.12.0
|
||||
- git-diff-check
|
||||
- gitleaks@8.30.0
|
||||
- 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 \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-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/* \
|
||||
&& pip install --no-cache-dir -U platformio \
|
||||
&& mkdir /tmp/firmware
|
||||
@@ -53,7 +53,7 @@ USER root
|
||||
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 \
|
||||
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/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
&& mkdir -p /etc/meshtasticd/config.d \
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ RUN apk --no-cache add \
|
||||
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
||||
libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||
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/* \
|
||||
&& pip install --no-cache-dir -U platformio \
|
||||
&& mkdir /tmp/firmware
|
||||
@@ -42,7 +42,7 @@ USER root
|
||||
|
||||
RUN apk --no-cache add \
|
||||
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/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
&& mkdir -p /etc/meshtasticd/config.d \
|
||||
|
||||
@@ -105,8 +105,6 @@ Lora:
|
||||
|
||||
GPS:
|
||||
# SerialPath: /dev/ttyS0
|
||||
# ExtraPins:
|
||||
# - 22
|
||||
|
||||
### Specify I2C device, or leave blank for none
|
||||
|
||||
|
||||
@@ -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=!"
|
||||
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
|
||||
|
||||
@REM Determine OTA filename based on MCU type (unified OTA format)
|
||||
SET "OTA_FILENAME=mt-!MCU!-ota.bin"
|
||||
IF "__!MCU!__" == "__esp32s3__" (
|
||||
@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!"
|
||||
|
||||
@REM Set SPIFFS filename with "littlefs-" prefix.
|
||||
|
||||
+14
-21
@@ -32,19 +32,6 @@ if ! command -v jq >/dev/null 2>&1; then
|
||||
exit 1
|
||||
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
|
||||
|
||||
# Usage info
|
||||
@@ -96,8 +83,8 @@ while [ $# -gt 0 ]; do
|
||||
done
|
||||
|
||||
if [[ $BPS_RESET == true ]]; then
|
||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS}
|
||||
exit 0
|
||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ -z "$FILENAME" ] && [ -n "$1" ] && {
|
||||
@@ -131,8 +118,14 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine OTA filename based on MCU type (unified OTA format)
|
||||
OTAFILE="mt-${MCU}-ota.bin"
|
||||
# Determine OTA filename based on MCU type
|
||||
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.
|
||||
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
|
||||
@@ -151,12 +144,12 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
||||
fi
|
||||
|
||||
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
|
||||
$ESPTOOL_CMD ${ESPTOOL_ERASE_FLASH}
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $FIRMWARE_OFFSET "${FILENAME}"
|
||||
$ESPTOOL_CMD erase-flash
|
||||
$ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}"
|
||||
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}"
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OFFSET "${SPIFFSFILE}"
|
||||
$ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}"
|
||||
|
||||
else
|
||||
show_help
|
||||
|
||||
+2
-13
@@ -20,17 +20,6 @@ else
|
||||
exit 1
|
||||
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
|
||||
show_help() {
|
||||
cat << EOF
|
||||
@@ -80,7 +69,7 @@ done
|
||||
shift "$((OPTIND-1))"
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -91,7 +80,7 @@ fi
|
||||
|
||||
if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then
|
||||
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
|
||||
show_help
|
||||
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]
|
||||
Description=Meshtastic %i Daemon
|
||||
Description=Meshtastic Native Daemon
|
||||
After=network-online.target
|
||||
StartLimitInterval=200
|
||||
StartLimitBurst=5
|
||||
@@ -9,7 +9,7 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
User=meshtasticd
|
||||
Group=meshtasticd
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/meshtasticd-start.sh %i
|
||||
ExecStart=/usr/bin/meshtasticd
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cp "release/meshtasticd_linux_$(uname -m)" /usr/bin/meshtasticd
|
||||
cp "bin/meshtasticd-start.sh" /usr/bin/meshtasticd-start.sh
|
||||
mkdir -p /etc/meshtasticd
|
||||
if [[ -f "/etc/meshtasticd/config.yaml" ]]; then
|
||||
cp bin/config-dist.yaml /etc/meshtasticd/config-upgrade.yaml
|
||||
|
||||
@@ -87,12 +87,6 @@
|
||||
</screenshots>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -58,16 +58,7 @@ def manifest_gather(source, target, env):
|
||||
manifest_ran = True
|
||||
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 = [
|
||||
progname,
|
||||
f"{progname}.elf",
|
||||
@@ -78,9 +69,7 @@ def manifest_gather(source, target, env):
|
||||
f"{progname}.uf2",
|
||||
f"{progname}.factory.uf2",
|
||||
f"{progname}.zip",
|
||||
lfsbin,
|
||||
f"mt-{board_mcu}-ota.bin",
|
||||
"bleota-c3.bin"
|
||||
lfsbin
|
||||
]
|
||||
for p in check_paths:
|
||||
f = env.File(env.subst(f"$BUILD_DIR/{p}"))
|
||||
@@ -93,9 +82,6 @@ def manifest_gather(source, target, env):
|
||||
"md5": f.get_content_hash(), # Returns MD5 hash
|
||||
"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)
|
||||
print(d)
|
||||
manifest_write(out, env)
|
||||
|
||||
@@ -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"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
"svd_path": "nrf52840.svd"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Minesemi ME25LS01",
|
||||
|
||||
@@ -33,8 +33,7 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
"svd_path": "nrf52840.svd"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"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"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
"svd_path": "nrf52840.svd"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "MINEWSEMI_MS24SF1_SX1262",
|
||||
|
||||
@@ -32,8 +32,7 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
"svd_path": "nrf52840.svd"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "BQ nRF52840",
|
||||
|
||||
@@ -33,8 +33,7 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
"svd_path": "nrf52840.svd"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "ProMicro compatible nRF52840",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DT_WATCH_S3",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
|
||||
@@ -33,8 +33,7 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
"svd_path": "nrf52840.svd"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Seeed T1000-E",
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
"svd_path": "nrf52840.svd"
|
||||
},
|
||||
"frameworks": ["arduino", "freertos"],
|
||||
"name": "Seeed WIO WM1110",
|
||||
|
||||
@@ -32,8 +32,7 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
"svd_path": "nrf52840.svd"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Seeed WIO WM1110",
|
||||
|
||||
@@ -32,8 +32,7 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
"svd_path": "nrf52840.svd"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Seeed WIO WM1110",
|
||||
|
||||
Vendored
-12
@@ -1,15 +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
|
||||
|
||||
Vendored
+1
-3
@@ -25,9 +25,7 @@ Build-Depends: debhelper-compat (= 13),
|
||||
liborcania-dev,
|
||||
libx11-dev,
|
||||
libinput-dev,
|
||||
libxkbcommon-x11-dev,
|
||||
libsqlite3-dev,
|
||||
libsdl2-dev
|
||||
libxkbcommon-x11-dev
|
||||
Standards-Version: 4.6.2
|
||||
Homepage: https://github.com/meshtastic/firmware
|
||||
Rules-Requires-Root: no
|
||||
|
||||
Vendored
-1
@@ -4,6 +4,5 @@ bin/config.yaml etc/meshtasticd
|
||||
bin/config.d/* etc/meshtasticd/available.d
|
||||
|
||||
bin/meshtasticd.service lib/systemd/system
|
||||
bin/meshtasticd-start.sh usr/bin
|
||||
|
||||
web/* usr/share/meshtasticd/web
|
||||
|
||||
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: libi2c-devel
|
||||
BuildRequires: pkgconfig(libuv)
|
||||
BuildRequires: pkgconfig(sqlite3)
|
||||
# Web components:
|
||||
BuildRequires: pkgconfig(openssl)
|
||||
BuildRequires: pkgconfig(liborcania)
|
||||
@@ -49,7 +48,6 @@ BuildRequires: pkgconfig(libulfius)
|
||||
BuildRequires: pkgconfig(x11)
|
||||
BuildRequires: pkgconfig(libinput)
|
||||
BuildRequires: pkgconfig(xkbcommon-x11)
|
||||
BuildRequires: pkgconfig(sdl2)
|
||||
|
||||
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
|
||||
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
|
||||
@@ -60,14 +58,8 @@ BuildRequires: pkgconfig(libbsd-overlay)
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
%prep
|
||||
@@ -103,9 +95,6 @@ cp -r bin/config.d/* %{buildroot}%{_sysconfdir}/meshtasticd/available.d
|
||||
# Install systemd 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
|
||||
mkdir -p %{buildroot}%{_datadir}/meshtasticd/web
|
||||
cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web
|
||||
@@ -158,7 +147,6 @@ fi
|
||||
%license LICENSE
|
||||
%doc README.md
|
||||
%{_bindir}/meshtasticd
|
||||
%{_bindir}/meshtasticd-start.sh
|
||||
%dir %{_localstatedir}/lib/meshtasticd
|
||||
%{_udevrulesdir}/99-meshtasticd-udev.rules
|
||||
%dir %{_sysconfdir}/meshtasticd
|
||||
|
||||
@@ -43,11 +43,13 @@ class Esp32C3ExceptionDecoder(DeviceMonitorFilterBase):
|
||||
self.enabled = self.setup_paths()
|
||||
|
||||
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.
|
||||
See https://docs.platformio.org/page/projectconf/build_configurations.html
|
||||
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
+10
-43
@@ -50,15 +50,11 @@ build_flags = -Wno-missing-field-initializers
|
||||
-DRADIOLIB_EXCLUDE_APRS=1
|
||||
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
||||
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
||||
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
|
||||
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
||||
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||
-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
|
||||
-DLED_BUILTIN=-1
|
||||
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
||||
#-D OLED_PL=1
|
||||
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
|
||||
@@ -68,7 +64,7 @@ monitor_speed = 115200
|
||||
monitor_filters = direct
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/b34c6817c25d6faabb3a8a162b5d14fb75395433.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||
@@ -117,12 +113,13 @@ lib_deps =
|
||||
[radiolib_base]
|
||||
lib_deps =
|
||||
# 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]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/6c75195e9987b7a49563232234f2f868dd343cae.zip
|
||||
https://github.com/meshtastic/device-ui/archive/272defcb35651461830ebfd1b39c9167c8f49317.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
@@ -132,7 +129,7 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor
|
||||
adafruit/Adafruit Unified Sensor@1.1.15
|
||||
# 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
|
||||
adafruit/Adafruit BMP085 Library@1.2.4
|
||||
# renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library
|
||||
@@ -145,8 +142,10 @@ lib_deps =
|
||||
adafruit/Adafruit INA260 Library@1.5.3
|
||||
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
||||
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
|
||||
adafruit/Adafruit MPU6050@2.2.9
|
||||
adafruit/Adafruit MPU6050@2.2.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
|
||||
adafruit/Adafruit LIS3DH@1.3.0
|
||||
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
|
||||
@@ -168,7 +167,7 @@ lib_deps =
|
||||
# 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
|
||||
# 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
|
||||
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
|
||||
@@ -212,38 +211,6 @@ lib_deps =
|
||||
# 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
|
||||
sensirion/Sensirion Core@0.7.2
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||
# 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...f78b3f0dcc
@@ -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 "configuration.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#ifdef HAS_NCP5623
|
||||
#include <NCP5623.h>
|
||||
#include <graphics/RAKled.h>
|
||||
NCP5623 rgb;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_LP5562
|
||||
#include <graphics/NomadStarLED.h>
|
||||
LP5562 rgbw;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_NEOPIXEL
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#include <graphics/NeoPixel.h>
|
||||
Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
|
||||
#endif
|
||||
|
||||
#ifdef UNPHONE
|
||||
@@ -23,24 +21,10 @@
|
||||
extern unPhone unphone;
|
||||
#endif
|
||||
|
||||
namespace concurrency
|
||||
{
|
||||
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:
|
||||
explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting")
|
||||
{
|
||||
@@ -52,15 +36,14 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
moduleConfig.ambient_lighting.led_state = true;
|
||||
#endif
|
||||
#endif
|
||||
#if AMBIENT_LIGHTING_TEST
|
||||
// define to enable test
|
||||
moduleConfig.ambient_lighting.led_state = true;
|
||||
moduleConfig.ambient_lighting.current = 10;
|
||||
// Uncomment to test module
|
||||
// moduleConfig.ambient_lighting.led_state = true;
|
||||
// moduleConfig.ambient_lighting.current = 10;
|
||||
// Default to a color based on our node number
|
||||
moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
||||
moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
||||
moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
||||
#endif
|
||||
// moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
||||
// moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
||||
// moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
||||
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
_type = type;
|
||||
if (_type == ScanI2C::DeviceType::NONE) {
|
||||
@@ -70,6 +53,11 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
}
|
||||
#endif
|
||||
#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");
|
||||
#ifdef HAS_NCP5623
|
||||
if (_type == ScanI2C::NCP5623) {
|
||||
@@ -89,13 +77,7 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
pixels.clear(); // Set all pixel colors to 'off'
|
||||
pixels.setBrightness(moduleConfig.ambient_lighting.current);
|
||||
#endif
|
||||
if (!moduleConfig.ambient_lighting.led_state) {
|
||||
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);
|
||||
setLighting();
|
||||
#endif
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
}
|
||||
@@ -109,8 +91,7 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) {
|
||||
#endif
|
||||
setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
setLighting();
|
||||
return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
}
|
||||
@@ -167,53 +148,65 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected:
|
||||
void setLighting(float current, uint8_t red, uint8_t green, uint8_t blue)
|
||||
void setLighting()
|
||||
{
|
||||
#ifdef HAS_NCP5623
|
||||
rgb.setCurrent(current);
|
||||
rgb.setRed(red);
|
||||
rgb.setGreen(green);
|
||||
rgb.setBlue(blue);
|
||||
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
|
||||
rgb.setCurrent(moduleConfig.ambient_lighting.current);
|
||||
rgb.setRed(moduleConfig.ambient_lighting.red);
|
||||
rgb.setGreen(moduleConfig.ambient_lighting.green);
|
||||
rgb.setBlue(moduleConfig.ambient_lighting.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
|
||||
#ifdef HAS_LP5562
|
||||
rgbw.setCurrent(current);
|
||||
rgbw.setRed(red);
|
||||
rgbw.setGreen(green);
|
||||
rgbw.setBlue(blue);
|
||||
LOG_DEBUG("Init LP5562 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
|
||||
rgbw.setCurrent(moduleConfig.ambient_lighting.current);
|
||||
rgbw.setRed(moduleConfig.ambient_lighting.red);
|
||||
rgbw.setGreen(moduleConfig.ambient_lighting.green);
|
||||
rgbw.setBlue(moduleConfig.ambient_lighting.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
|
||||
#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
|
||||
// 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)
|
||||
pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1);
|
||||
#endif
|
||||
#if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX)
|
||||
pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1);
|
||||
#endif
|
||||
#endif
|
||||
pixels.show();
|
||||
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%f, red=%d, green=%d, blue=%d",
|
||||
// current, red, green, blue);
|
||||
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(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
|
||||
#ifdef RGBLED_CA
|
||||
analogWrite(RGBLED_RED, 255 - red);
|
||||
analogWrite(RGBLED_GREEN, 255 - green);
|
||||
analogWrite(RGBLED_BLUE, 255 - blue);
|
||||
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||
analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);
|
||||
analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green);
|
||||
analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.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)
|
||||
analogWrite(RGBLED_RED, red);
|
||||
analogWrite(RGBLED_GREEN, green);
|
||||
analogWrite(RGBLED_BLUE, blue);
|
||||
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||
analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red);
|
||||
analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green);
|
||||
analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.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
|
||||
#ifdef UNPHONE
|
||||
unphone.rgb(red, green, blue);
|
||||
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||
unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
|
||||
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 // AMBIENTLIGHTINGTHREAD_H
|
||||
|
||||
} // namespace concurrency
|
||||
|
||||
+10
-2
@@ -89,14 +89,22 @@ class BluetoothStatus : public Status
|
||||
case ConnectionState::CONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||
#ifdef BLE_LED
|
||||
digitalWrite(BLE_LED, LED_STATE_ON);
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#else
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#endif
|
||||
#endif
|
||||
break;
|
||||
|
||||
case ConnectionState::DISCONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||
#ifdef BLE_LED
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#else
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#endif
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -41,8 +41,7 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...)
|
||||
}
|
||||
|
||||
#if HAS_NETWORKING
|
||||
namespace meshtastic
|
||||
{
|
||||
|
||||
Syslog::Syslog(UDP &client)
|
||||
{
|
||||
this->_client = &client;
|
||||
@@ -196,6 +195,4 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
|
||||
return true;
|
||||
}
|
||||
|
||||
}; // namespace meshtastic
|
||||
|
||||
#endif
|
||||
|
||||
@@ -162,8 +162,6 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
|
||||
|
||||
#if HAS_NETWORKING
|
||||
|
||||
namespace meshtastic
|
||||
{
|
||||
class Syslog
|
||||
{
|
||||
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)));
|
||||
};
|
||||
|
||||
}; // namespace meshtastic
|
||||
|
||||
#endif // HAS_NETWORKING
|
||||
#endif // HAS_NETWORKING
|
||||
+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
-89
@@ -13,11 +13,6 @@
|
||||
#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;
|
||||
@@ -107,60 +102,6 @@ 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)
|
||||
{
|
||||
@@ -190,11 +131,6 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
|
||||
}
|
||||
|
||||
addLiveMessage(sm);
|
||||
|
||||
#if ENABLE_MESSAGE_PERSISTENCE
|
||||
markMessageStoreUnsaved();
|
||||
#endif
|
||||
|
||||
return liveMessages.back();
|
||||
}
|
||||
|
||||
@@ -219,10 +155,6 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st
|
||||
sm.ackStatus = AckStatus::NONE;
|
||||
|
||||
addLiveMessage(sm);
|
||||
|
||||
#if ENABLE_MESSAGE_PERSISTENCE
|
||||
markMessageStoreUnsaved();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ENABLE_MESSAGE_PERSISTENCE
|
||||
@@ -307,10 +239,6 @@ void MessageStore::saveToFlash()
|
||||
|
||||
f.close();
|
||||
#endif
|
||||
|
||||
// Reset autosave state after any save
|
||||
g_messageStoreHasUnsavedChanges = false;
|
||||
g_lastAutoSaveMs = millis();
|
||||
}
|
||||
|
||||
void MessageStore::loadFromFlash()
|
||||
@@ -342,9 +270,6 @@ void MessageStore::loadFromFlash()
|
||||
|
||||
f.close();
|
||||
#endif
|
||||
// Loading messages does not trigger an autosave
|
||||
g_messageStoreHasUnsavedChanges = false;
|
||||
g_lastAutoSaveMs = millis();
|
||||
}
|
||||
|
||||
#else
|
||||
@@ -365,11 +290,6 @@ void MessageStore::clearAllMessages()
|
||||
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
|
||||
@@ -501,14 +421,6 @@ uint16_t MessageStore::storeText(const char *src, size_t len)
|
||||
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
|
||||
#endif
|
||||
@@ -125,11 +125,6 @@ class MessageStore
|
||||
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;
|
||||
|
||||
|
||||
+97
-349
@@ -1,14 +1,11 @@
|
||||
/**
|
||||
* @file Power.cpp
|
||||
* @brief This file contains the implementation of the Power class, which is
|
||||
* responsible for managing power-related functionality of the device. It
|
||||
* includes battery level sensing, power management unit (PMU) control, and
|
||||
* power state machine management. The Power class is used by the main device
|
||||
* class to manage power-related functionality.
|
||||
* @brief This file contains the implementation of the Power class, which is responsible for managing power-related functionality
|
||||
* of the device. It includes battery level sensing, power management unit (PMU) control, and power state machine management. The
|
||||
* Power class is used by the main device class to manage power-related functionality.
|
||||
*
|
||||
* The file also includes implementations of various battery level sensors, such
|
||||
* as the AnalogBatteryLevel class, which assumes the battery voltage is
|
||||
* attached via a voltage-divider to an analog input.
|
||||
* The file also includes implementations of various battery level sensors, such as the AnalogBatteryLevel class, which assumes
|
||||
* the battery voltage is attached via a voltage-divider to an analog input.
|
||||
*
|
||||
* This file is part of the Meshtastic project.
|
||||
* For more information, see: https://meshtastic.org/
|
||||
@@ -22,7 +19,6 @@
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "power/PowerHAL.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
@@ -175,12 +171,22 @@ Power *power;
|
||||
|
||||
using namespace meshtastic;
|
||||
|
||||
// NRF52 has AREF_VOLTAGE defined in architecture.h but
|
||||
// make sure it's included. If something is wrong with NRF52
|
||||
// definition - compilation will fail on missing definition
|
||||
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52)
|
||||
#ifndef AREF_VOLTAGE
|
||||
#if defined(ARCH_NRF52)
|
||||
/*
|
||||
* Internal Reference is +/-0.6V, with an adjustable gain of 1/6, 1/5, 1/4,
|
||||
* 1/3, 1/2 or 1, meaning 3.6, 3.0, 2.4, 1.8, 1.2 or 0.6V for the ADC levels.
|
||||
*
|
||||
* External Reference is VDD/4, with an adjustable gain of 1, 2 or 4, meaning
|
||||
* VDD/4, VDD/2 or VDD for the ADC levels.
|
||||
*
|
||||
* Default settings are internal reference with 1/6 gain (GND..3.6V ADC range)
|
||||
*/
|
||||
#define AREF_VOLTAGE 3.6
|
||||
#else
|
||||
#define AREF_VOLTAGE 3.3
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/**
|
||||
* If this board has a battery level sensor, set this to a valid implementation
|
||||
@@ -227,8 +233,7 @@ static void battery_adcDisable()
|
||||
#endif
|
||||
|
||||
/**
|
||||
* A simple battery level sensor that assumes the battery voltage is attached
|
||||
* via a voltage-divider to an analog input
|
||||
* A simple battery level sensor that assumes the battery voltage is attached via a voltage-divider to an analog input
|
||||
*/
|
||||
class AnalogBatteryLevel : public HasBatteryLevel
|
||||
{
|
||||
@@ -306,8 +311,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
|
||||
#ifndef BATTERY_SENSE_SAMPLES
|
||||
#define BATTERY_SENSE_SAMPLES \
|
||||
15 // Set the number of samples, it has an effect of increasing sensitivity in
|
||||
// complex electromagnetic environment.
|
||||
15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment.
|
||||
#endif
|
||||
|
||||
#ifdef BATTERY_PIN
|
||||
@@ -337,8 +341,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
battery_adcDisable();
|
||||
|
||||
if (!initial_read_done) {
|
||||
// Flush the smoothing filter with an ADC reading, if the reading is
|
||||
// plausibly correct
|
||||
// Flush the smoothing filter with an ADC reading, if the reading is plausibly correct
|
||||
if (scaled > last_read_value)
|
||||
last_read_value = scaled;
|
||||
initial_read_done = true;
|
||||
@@ -347,8 +350,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF
|
||||
}
|
||||
|
||||
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u",
|
||||
// BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) (last_read_value));
|
||||
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t)
|
||||
// (last_read_value));
|
||||
}
|
||||
return last_read_value;
|
||||
#endif // BATTERY_PIN
|
||||
@@ -417,8 +420,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
/**
|
||||
* return true if there is a battery installed in this unit
|
||||
*/
|
||||
// if we have a integrated device with a battery, we can assume that the
|
||||
// battery is always connected
|
||||
// if we have a integrated device with a battery, we can assume that the battery is always connected
|
||||
#ifdef BATTERY_IMMUTABLE
|
||||
virtual bool isBatteryConnect() override { return true; }
|
||||
#elif defined(ADC_V)
|
||||
@@ -439,10 +441,10 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
|
||||
#endif
|
||||
|
||||
/// If we see a battery voltage higher than physics allows - assume charger is
|
||||
/// pumping in power On some boards we don't have the power management chip
|
||||
/// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power
|
||||
/// source
|
||||
/// If we see a battery voltage higher than physics allows - assume charger is pumping
|
||||
/// in power
|
||||
/// On some boards we don't have the power management chip (like AXPxxxx)
|
||||
/// so we use EXT_PWR_DETECT GPIO pin to detect external power source
|
||||
virtual bool isVbusIn() override
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
@@ -459,14 +461,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
}
|
||||
// if it's not HIGH - check the battery
|
||||
#endif
|
||||
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
|
||||
return false;
|
||||
|
||||
// technically speaking this should work for all(?) NRF52 boards
|
||||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||||
// VBUS was not properly connected and detected by the CPU
|
||||
#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO)
|
||||
return powerHAL_isVBUSConnected();
|
||||
#elif defined(MUZI_BASE)
|
||||
return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk;
|
||||
#endif
|
||||
return getBattVoltage() > chargingVolt;
|
||||
}
|
||||
@@ -480,18 +476,15 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse;
|
||||
}
|
||||
#endif
|
||||
#if defined(ELECROW_ThinkNode_M6)
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn();
|
||||
#elif EXT_CHRG_DETECT
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||
#elif defined(BATTERY_CHARGING_INV)
|
||||
return !digitalRead(BATTERY_CHARGING_INV);
|
||||
#else
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
|
||||
if (hasINA()) {
|
||||
// get current flow from INA sensor - negative value means power flowing
|
||||
// into the battery default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT
|
||||
// RESISTOR <--> INA_VIN- <--> LOAD
|
||||
// get current flow from INA sensor - negative value means power flowing into the battery
|
||||
// default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD
|
||||
LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address);
|
||||
#if defined(INA_CHARGING_DETECTION_INVERT)
|
||||
return getINACurrent() > 0;
|
||||
@@ -507,8 +500,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
}
|
||||
|
||||
private:
|
||||
/// If we see a battery voltage higher than physics allows - assume charger is
|
||||
/// pumping in power
|
||||
/// If we see a battery voltage higher than physics allows - assume charger is pumping
|
||||
/// in power
|
||||
|
||||
/// For heltecs with no battery connected, the measured voltage is 2204, so
|
||||
// need to be higher than that, in this case is 2500mV (3000-500)
|
||||
@@ -517,8 +510,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
|
||||
// Start value from minimum voltage for the filter to not start from 0
|
||||
// that could trigger some events.
|
||||
// This value is over-written by the first ADC reading, it the voltage seems
|
||||
// reasonable.
|
||||
// This value is over-written by the first ADC reading, it the voltage seems reasonable.
|
||||
bool initial_read_done = false;
|
||||
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
||||
uint32_t last_read_time_ms = 0;
|
||||
@@ -660,8 +652,7 @@ bool Power::analogInit()
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||
// ESP32S3
|
||||
else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) {
|
||||
LOG_INFO("ADC config based on Two Point values and fitting curve "
|
||||
"coefficients stored in eFuse");
|
||||
LOG_INFO("ADC config based on Two Point values and fitting curve coefficients stored in eFuse");
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
@@ -669,7 +660,13 @@ bool Power::analogInit()
|
||||
}
|
||||
#endif // ARCH_ESP32
|
||||
|
||||
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
||||
#ifdef ARCH_NRF52
|
||||
#ifdef VBAT_AR_INTERNAL
|
||||
analogReference(VBAT_AR_INTERNAL);
|
||||
#else
|
||||
analogReference(AR_INTERNAL); // 3.6V
|
||||
#endif
|
||||
#endif // ARCH_NRF52
|
||||
|
||||
#ifndef ARCH_ESP32
|
||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||
@@ -692,14 +689,10 @@ bool Power::setup()
|
||||
bool found = false;
|
||||
if (axpChipInit()) {
|
||||
found = true;
|
||||
} else if (cw2015Init()) {
|
||||
found = true;
|
||||
} else if (max17048Init()) {
|
||||
} else if (lipoInit()) {
|
||||
found = true;
|
||||
} else if (lipoChargerInit()) {
|
||||
found = true;
|
||||
} else if (serialBatteryInit()) {
|
||||
found = true;
|
||||
} else if (meshSolarInit()) {
|
||||
found = true;
|
||||
} else if (analogInit()) {
|
||||
@@ -726,16 +719,6 @@ bool Power::setup()
|
||||
runASAP = true;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
attachInterrupt(
|
||||
EXT_CHRG_DETECT,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
enabled = found;
|
||||
low_voltage_counter = 0;
|
||||
@@ -782,8 +765,7 @@ void Power::reboot()
|
||||
HAL_NVIC_SystemReset();
|
||||
#else
|
||||
rebootAtMsec = -1;
|
||||
LOG_WARN("FIXME implement reboot for this platform. Note that some settings "
|
||||
"require a restart to be applied");
|
||||
LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -793,12 +775,9 @@ void Power::shutdown()
|
||||
#if HAS_SCREEN
|
||||
if (screen) {
|
||||
#ifdef T_DECK_PRO
|
||||
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!",
|
||||
0); // T-Deck Pro has no power button
|
||||
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button
|
||||
#elif defined(USE_EINK)
|
||||
screen->showSimpleBanner("Shutting Down...",
|
||||
2250); // dismiss after 3 seconds to avoid the
|
||||
// banner on the sleep screen
|
||||
screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen
|
||||
#else
|
||||
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
|
||||
#endif
|
||||
@@ -820,9 +799,6 @@ void Power::shutdown()
|
||||
#endif
|
||||
#ifdef PIN_LED3
|
||||
ledOff(PIN_LED3);
|
||||
#endif
|
||||
#ifdef LED_NOTIFICATION
|
||||
ledOff(LED_NOTIFICATION);
|
||||
#endif
|
||||
doDeepSleep(DELAY_FOREVER, true, true);
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
@@ -840,8 +816,7 @@ void Power::readPowerStatus()
|
||||
int32_t batteryVoltageMv = -1; // Assume unknown
|
||||
int8_t batteryChargePercent = -1;
|
||||
OptionalBool usbPowered = OptUnknown;
|
||||
OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM
|
||||
// code doesn't run every time
|
||||
OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM code doesn't run every time
|
||||
OptionalBool isChargingNow = OptUnknown;
|
||||
|
||||
if (batteryLevel) {
|
||||
@@ -854,10 +829,9 @@ void Power::readPowerStatus()
|
||||
if (batteryLevel->getBatteryPercent() >= 0) {
|
||||
batteryChargePercent = batteryLevel->getBatteryPercent();
|
||||
} else {
|
||||
// If the AXP192 returns a percentage less than 0, the feature is either
|
||||
// not supported or there is an error In that case, we compute an
|
||||
// estimate of the charge percent based on open circuit voltage table
|
||||
// defined in power.h
|
||||
// If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error
|
||||
// In that case, we compute an estimate of the charge percent based on open circuit voltage table defined
|
||||
// in power.h
|
||||
batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) /
|
||||
((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))),
|
||||
0, 100);
|
||||
@@ -865,12 +839,12 @@ void Power::readPowerStatus()
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way
|
||||
// better instead to make a Nrf52IsUsbPowered subclass (which shares a
|
||||
// superclass with the BatteryLevel stuff) that just provides a few methods. But
|
||||
// in the interest of fixing this bug I'm going to follow current practice.
|
||||
#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates
|
||||
// the power states. Takes 20 seconds or so to detect changes.
|
||||
// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way better instead to make a Nrf52IsUsbPowered subclass
|
||||
// (which shares a superclass with the BatteryLevel stuff)
|
||||
// that just provides a few methods. But in the interest of fixing this bug I'm going to follow current
|
||||
// practice.
|
||||
#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates the power states. Takes 20 seconds or so to detect
|
||||
// changes.
|
||||
|
||||
nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get();
|
||||
// LOG_DEBUG("NRF Power %d", nrf_usb_state);
|
||||
@@ -944,9 +918,8 @@ void Power::readPowerStatus()
|
||||
|
||||
#endif
|
||||
|
||||
// If we have a battery at all and it is less than 0%, force deep sleep if we
|
||||
// have more than 10 low readings in a row. NOTE: min LiIon/LiPo voltage
|
||||
// is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
|
||||
// If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in
|
||||
// a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
|
||||
//
|
||||
|
||||
if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) {
|
||||
@@ -968,8 +941,8 @@ int32_t Power::runOnce()
|
||||
readPowerStatus();
|
||||
|
||||
#ifdef HAS_PMU
|
||||
// WE no longer use the IRQ line to wake the CPU (due to false wakes from
|
||||
// sleep), but we do poll the IRQ status by reading the registers over I2C
|
||||
// WE no longer use the IRQ line to wake the CPU (due to false wakes from sleep), but we do poll
|
||||
// the IRQ status by reading the registers over I2C
|
||||
if (PMU) {
|
||||
|
||||
PMU->getIrqStatus();
|
||||
@@ -1011,8 +984,7 @@ int32_t Power::runOnce()
|
||||
PMU->clearIrqStatus();
|
||||
}
|
||||
#endif
|
||||
// Only read once every 20 seconds once the power status for the app has been
|
||||
// initialized
|
||||
// Only read once every 20 seconds once the power status for the app has been initialized
|
||||
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
|
||||
}
|
||||
|
||||
@@ -1020,12 +992,10 @@ int32_t Power::runOnce()
|
||||
* Init the power manager chip
|
||||
*
|
||||
* axp192 power
|
||||
DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose
|
||||
comms to the axp192 because the OLED and the axp192 share the same i2c bus,
|
||||
instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max ->
|
||||
ESP32 (keep this on!) LDO1 30mA -> charges GPS backup battery // charges the
|
||||
tiny J13 battery by the GPS to power the GPS ram (for a couple of days), can
|
||||
not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
|
||||
DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms to the axp192 because the OLED and the
|
||||
axp192 share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this
|
||||
on!) LDO1 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for a couple of
|
||||
days), can not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
|
||||
*
|
||||
*/
|
||||
bool Power::axpChipInit()
|
||||
@@ -1070,10 +1040,9 @@ bool Power::axpChipInit()
|
||||
|
||||
if (!PMU) {
|
||||
/*
|
||||
* In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will
|
||||
* be called at the same time. In order not to affect other devices, if the
|
||||
* initialization of the PMU fails, Wire needs to be re-initialized once, if
|
||||
* there are multiple devices sharing the bus.
|
||||
* In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will be called at the same time.
|
||||
* In order not to affect other devices, if the initialization of the PMU fails, Wire needs to be re-initialized once,
|
||||
* if there are multiple devices sharing the bus.
|
||||
* * */
|
||||
#ifndef PMU_USE_WIRE1
|
||||
w->begin(I2C_SDA, I2C_SCL);
|
||||
@@ -1090,8 +1059,8 @@ bool Power::axpChipInit()
|
||||
PMU->enablePowerOutput(XPOWERS_LDO2);
|
||||
|
||||
// oled module power channel,
|
||||
// disable it will cause abnormal communication between boot and AXP power
|
||||
// supply, do not turn it off
|
||||
// disable it will cause abnormal communication between boot and AXP power supply,
|
||||
// do not turn it off
|
||||
PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
|
||||
// enable oled power
|
||||
PMU->enablePowerOutput(XPOWERS_DCDC1);
|
||||
@@ -1118,8 +1087,7 @@ bool Power::axpChipInit()
|
||||
PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
|
||||
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
||||
|
||||
/*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it
|
||||
* uses an AXP2101 power chip*/
|
||||
/*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it uses an AXP2101 power chip*/
|
||||
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
|
||||
// Unuse power channel
|
||||
PMU->disablePowerOutput(XPOWERS_DCDC2);
|
||||
@@ -1154,8 +1122,8 @@ bool Power::axpChipInit()
|
||||
// t-beam s3 core
|
||||
/**
|
||||
* gnss module power channel
|
||||
* The default ALDO4 is off, you need to turn on the GNSS power first,
|
||||
* otherwise it will be invalid during initialization
|
||||
* The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during
|
||||
* initialization
|
||||
*/
|
||||
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
|
||||
PMU->enablePowerOutput(XPOWERS_ALDO4);
|
||||
@@ -1205,8 +1173,7 @@ bool Power::axpChipInit()
|
||||
// disable all axp chip interrupt
|
||||
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
|
||||
|
||||
// Set the constant current charging current of AXP2101, temporarily use
|
||||
// 500mA by default
|
||||
// Set the constant current charging current of AXP2101, temporarily use 500mA by default
|
||||
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
|
||||
|
||||
// Set up the charging voltage
|
||||
@@ -1272,12 +1239,11 @@ bool Power::axpChipInit()
|
||||
PMU->getPowerChannelVoltage(XPOWERS_BLDO2));
|
||||
}
|
||||
|
||||
// We can safely ignore this approach for most (or all) boards because MCU
|
||||
// turned off earlier than battery discharged to 2.6V.
|
||||
// We can safely ignore this approach for most (or all) boards because MCU turned off
|
||||
// earlier than battery discharged to 2.6V.
|
||||
//
|
||||
// Unfortunately for now we can't use this killswitch for RAK4630-based boards
|
||||
// because they have a bug with battery voltage measurement. Probably it
|
||||
// sometimes drops to low values.
|
||||
// Unfortanly for now we can't use this killswitch for RAK4630-based boards because they have a bug with
|
||||
// battery voltage measurement. Probably it sometimes drops to low values.
|
||||
#ifndef RAK4630
|
||||
// Set PMU shutdown voltage at 2.6V to maximize battery utilization
|
||||
PMU->setSysPowerDownVoltage(2600);
|
||||
@@ -1296,12 +1262,10 @@ bool Power::axpChipInit()
|
||||
attachInterrupt(
|
||||
PMU_IRQ, [] { pmu_irq = true; }, FALLING);
|
||||
|
||||
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ
|
||||
// because it occurs repeatedly while there is no battery also it could cause
|
||||
// inadvertent waking from light sleep just because the battery filled we
|
||||
// don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while
|
||||
// no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we
|
||||
// don't have anything hooked to vbus
|
||||
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ because it occurs repeatedly while there is
|
||||
// no battery also it could cause inadvertent waking from light sleep just because the battery filled
|
||||
// we don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while no battery installed
|
||||
// we don't look at AXPXXX_VBUS_REMOVED_IRQ because we don't have anything hooked to vbus
|
||||
PMU->enableIRQ(pmuIrqMask);
|
||||
|
||||
PMU->clearIrqStatus();
|
||||
@@ -1323,7 +1287,7 @@ bool Power::axpChipInit()
|
||||
/**
|
||||
* Wrapper class for an I2C MAX17048 Lipo battery sensor.
|
||||
*/
|
||||
class MAX17048BatteryLevel : public HasBatteryLevel
|
||||
class LipoBatteryLevel : public HasBatteryLevel
|
||||
{
|
||||
private:
|
||||
MAX17048Singleton *max17048 = nullptr;
|
||||
@@ -1371,18 +1335,18 @@ class MAX17048BatteryLevel : public HasBatteryLevel
|
||||
virtual bool isCharging() override { return max17048->isBatteryCharging(); }
|
||||
};
|
||||
|
||||
MAX17048BatteryLevel max17048Level;
|
||||
LipoBatteryLevel lipoLevel;
|
||||
|
||||
/**
|
||||
* Init the Lipo battery level sensor
|
||||
*/
|
||||
bool Power::max17048Init()
|
||||
bool Power::lipoInit()
|
||||
{
|
||||
bool result = max17048Level.runOnce();
|
||||
LOG_DEBUG("Power::max17048Init lipo sensor is %s", result ? "ready" : "not ready yet");
|
||||
bool result = lipoLevel.runOnce();
|
||||
LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet");
|
||||
if (!result)
|
||||
return false;
|
||||
batteryLevel = &max17048Level;
|
||||
batteryLevel = &lipoLevel;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1390,88 +1354,7 @@ bool Power::max17048Init()
|
||||
/**
|
||||
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||
*/
|
||||
bool Power::max17048Init()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C && HAS_CW2015
|
||||
|
||||
class CW2015BatteryLevel : public AnalogBatteryLevel
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||||
*/
|
||||
virtual int getBatteryPercent() override
|
||||
{
|
||||
int data = -1;
|
||||
Wire.beginTransmission(CW2015_ADDR);
|
||||
Wire.write(0x04);
|
||||
if (Wire.endTransmission() == 0) {
|
||||
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
|
||||
data = Wire.read();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||||
*/
|
||||
virtual uint16_t getBattVoltage() override
|
||||
{
|
||||
uint16_t mv = 0;
|
||||
Wire.beginTransmission(CW2015_ADDR);
|
||||
Wire.write(0x02);
|
||||
if (Wire.endTransmission() == 0) {
|
||||
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)2)) {
|
||||
mv = Wire.read();
|
||||
mv <<= 8;
|
||||
mv |= Wire.read();
|
||||
// Voltage is read in 305uV units, convert to mV
|
||||
mv = mv * 305 / 1000;
|
||||
}
|
||||
}
|
||||
return mv;
|
||||
}
|
||||
};
|
||||
|
||||
CW2015BatteryLevel cw2015Level;
|
||||
|
||||
/**
|
||||
* Init the CW2015 battery level sensor
|
||||
*/
|
||||
bool Power::cw2015Init()
|
||||
{
|
||||
|
||||
Wire.beginTransmission(CW2015_ADDR);
|
||||
uint8_t getInfo[] = {0x0a, 0x00};
|
||||
Wire.write(getInfo, 2);
|
||||
Wire.endTransmission();
|
||||
delay(10);
|
||||
Wire.beginTransmission(CW2015_ADDR);
|
||||
Wire.write(0x00);
|
||||
bool result = false;
|
||||
if (Wire.endTransmission() == 0) {
|
||||
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
|
||||
uint8_t data = Wire.read();
|
||||
LOG_DEBUG("CW2015 init read data: 0x%x", data);
|
||||
if (data == 0x73) {
|
||||
result = true;
|
||||
batteryLevel = &cw2015Level;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
/**
|
||||
* The CW2015 battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||
*/
|
||||
bool Power::cw2015Init()
|
||||
bool Power::lipoInit()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1498,8 +1381,8 @@ class LipoCharger : public HasBatteryLevel
|
||||
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
|
||||
if (result) {
|
||||
LOG_INFO("PPM BQ25896 init succeeded");
|
||||
// Set the minimum operating voltage. Below this voltage, the PPM will
|
||||
// protect PPM->setSysPowerDownVoltage(3100);
|
||||
// Set the minimum operating voltage. Below this voltage, the PPM will protect
|
||||
// PPM->setSysPowerDownVoltage(3100);
|
||||
|
||||
// Set input current limit, default is 500mA
|
||||
// PPM->setInputCurrentLimit(800);
|
||||
@@ -1522,8 +1405,7 @@ class LipoCharger : public HasBatteryLevel
|
||||
PPM->enableMeasure();
|
||||
|
||||
// Turn on charging function
|
||||
// If there is no battery connected, do not turn on the charging
|
||||
// function
|
||||
// If there is no battery connected, do not turn on the charging function
|
||||
PPM->enableCharge();
|
||||
} else {
|
||||
LOG_WARN("PPM BQ25896 init failed");
|
||||
@@ -1558,8 +1440,7 @@ class LipoCharger : public HasBatteryLevel
|
||||
virtual int getBatteryPercent() override
|
||||
{
|
||||
return -1;
|
||||
// return bq->getChargePercent(); // don't use BQ27220 for battery percent,
|
||||
// it is not calibrated
|
||||
// return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1681,143 +1562,10 @@ bool Power::meshSolarInit()
|
||||
|
||||
#else
|
||||
/**
|
||||
* The meshSolar battery level sensor is unavailable - default to
|
||||
* AnalogBatteryLevel
|
||||
* The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||
*/
|
||||
bool Power::meshSolarInit()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAS_SERIAL_BATTERY_LEVEL
|
||||
#include <SoftwareSerial.h>
|
||||
|
||||
/**
|
||||
* SerialBatteryLevel class for pulling battery information from a secondary MCU over serial.
|
||||
*/
|
||||
class SerialBatteryLevel : public HasBatteryLevel
|
||||
{
|
||||
|
||||
public:
|
||||
/**
|
||||
* Init the I2C meshSolar battery level sensor
|
||||
*/
|
||||
bool runOnce()
|
||||
{
|
||||
BatterySerial.begin(4800);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||||
*/
|
||||
virtual int getBatteryPercent() override { return v_percent; }
|
||||
|
||||
/**
|
||||
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||||
*/
|
||||
virtual uint16_t getBattVoltage() override { return voltage * 1000; }
|
||||
|
||||
/**
|
||||
* return true if there is a battery installed in this unit
|
||||
*/
|
||||
virtual bool isBatteryConnect() override
|
||||
{
|
||||
// definitely need to gobble up more bytes at once
|
||||
if (BatterySerial.available() > 5) {
|
||||
// LOG_WARN("SerialBatteryLevel: %u bytes available", BatterySerial.available());
|
||||
while (BatterySerial.available() > 11) {
|
||||
BatterySerial.read(); // flush old data
|
||||
}
|
||||
// LOG_WARN("SerialBatteryLevel: %u bytes now available", BatterySerial.available());
|
||||
int tries = 0;
|
||||
while (BatterySerial.read() != 0xFE) {
|
||||
tries++; // wait for start byte
|
||||
if (tries > 10) {
|
||||
LOG_WARN("SerialBatteryLevel: no start byte found");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
Data[1] = BatterySerial.read();
|
||||
Data[2] = BatterySerial.read();
|
||||
Data[3] = BatterySerial.read();
|
||||
Data[4] = BatterySerial.read();
|
||||
Data[5] = BatterySerial.read();
|
||||
if (Data[5] != 0xFD) {
|
||||
LOG_WARN("SerialBatteryLevel: invalid end byte %02x", Data[5]);
|
||||
return true;
|
||||
}
|
||||
v_percent = Data[1];
|
||||
voltage = Data[2] + (((float)Data[3]) / 100) + (((float)Data[4]) / 10000);
|
||||
voltage *= 2;
|
||||
// LOG_WARN("SerialBatteryLevel: received data %u, %f, %02x", v_percent, voltage, Data[5]);
|
||||
return true;
|
||||
}
|
||||
// This function runs first, so use it to grab the latest data from the secondary MCU
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* return true if there is an external power source detected
|
||||
*/
|
||||
virtual bool isVbusIn() override
|
||||
{
|
||||
#if defined(EXT_CHRG_DETECT)
|
||||
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool isCharging() override
|
||||
{
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||
|
||||
#endif
|
||||
// by default, we check the battery voltage only
|
||||
return isVbusIn();
|
||||
}
|
||||
|
||||
private:
|
||||
SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX);
|
||||
uint8_t Data[6] = {0};
|
||||
int v_percent = 0;
|
||||
float voltage = 0.0;
|
||||
};
|
||||
|
||||
SerialBatteryLevel serialBatteryLevel;
|
||||
|
||||
/**
|
||||
* Init the serial battery level sensor
|
||||
*/
|
||||
bool Power::serialBatteryInit()
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
pinMode(EXT_PWR_DETECT, INPUT);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
|
||||
#endif
|
||||
|
||||
bool result = serialBatteryLevel.runOnce();
|
||||
LOG_DEBUG("Power::serialBatteryInit serial battery sensor is %s", result ? "ready" : "not ready yet");
|
||||
if (!result)
|
||||
return false;
|
||||
batteryLevel = &serialBatteryLevel;
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
/**
|
||||
* If this device has no serial battery level sensor, don't try to use it.
|
||||
*/
|
||||
bool Power::serialBatteryInit()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
+4
-4
@@ -9,13 +9,13 @@
|
||||
*/
|
||||
#include "PowerFSM.h"
|
||||
#include "Default.h"
|
||||
#include "Led.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "main.h"
|
||||
#include "modules/StatusLEDModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
@@ -103,7 +103,7 @@ static void lsIdle()
|
||||
uint32_t sleepTime = SLEEP_TIME;
|
||||
|
||||
powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep);
|
||||
statusLEDModule->setPowerLED(false);
|
||||
ledBlink.set(false); // Never leave led on while in light sleep
|
||||
esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL);
|
||||
powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep);
|
||||
|
||||
@@ -111,7 +111,7 @@ static void lsIdle()
|
||||
case ESP_SLEEP_WAKEUP_TIMER:
|
||||
// Normal case: timer expired, we should just go back to sleep ASAP
|
||||
|
||||
statusLEDModule->setPowerLED(true);
|
||||
ledBlink.set(true); // briefly turn on led
|
||||
wakeCause2 = doLightSleep(100); // leave led on for 1ms
|
||||
|
||||
secsSlept += sleepTime;
|
||||
@@ -146,7 +146,7 @@ static void lsIdle()
|
||||
}
|
||||
} else {
|
||||
// Time to stop sleeping!
|
||||
statusLEDModule->setPowerLED(false);
|
||||
ledBlink.set(false);
|
||||
LOG_INFO("Reached ls_secs, service loop()");
|
||||
powerFSM.trigger(EVENT_WAKE_TIMER);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#endif
|
||||
|
||||
#if HAS_NETWORKING
|
||||
extern meshtastic::Syslog syslog;
|
||||
extern Syslog syslog;
|
||||
#endif
|
||||
void RedirectablePrint::rpInit()
|
||||
{
|
||||
|
||||
+10
-2
@@ -54,7 +54,7 @@ size_t SafeFile::write(const uint8_t *buffer, size_t size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically close the file (overwriting any old version) and readback the contents to confirm the hash matches
|
||||
* Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches
|
||||
*
|
||||
* @return false for failure
|
||||
*/
|
||||
@@ -73,7 +73,15 @@ bool SafeFile::close()
|
||||
if (!testReadback())
|
||||
return false;
|
||||
|
||||
// Rename or overwrite (atomic operation)
|
||||
{ // Scope for lock
|
||||
concurrency::LockGuard g(spiLock);
|
||||
// brief window of risk here ;-)
|
||||
if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) {
|
||||
LOG_ERROR("Can't remove old pref file");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String filenameTmp = filename;
|
||||
filenameTmp += ".tmp";
|
||||
if (!renameFile(filenameTmp.c_str(), filename.c_str())) {
|
||||
|
||||
+23
-21
@@ -155,10 +155,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#endif
|
||||
|
||||
// Default system gain to 0 if not defined
|
||||
#ifndef NUM_PA_POINTS
|
||||
#define NUM_PA_POINTS 1
|
||||
#endif
|
||||
|
||||
#ifndef TX_GAIN_LORA
|
||||
#define TX_GAIN_LORA 0
|
||||
#endif
|
||||
@@ -176,12 +172,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// -----------------------------------------------------------------------------
|
||||
// OLED & Input
|
||||
// -----------------------------------------------------------------------------
|
||||
#define SSD1306_ADDRESS_L 0x3C // Addr = 0
|
||||
#define SSD1306_ADDRESS_H 0x3D // Addr = 1
|
||||
|
||||
#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK)
|
||||
#define SSD1306_ADDRESS SSD1306_ADDRESS_H
|
||||
#define SSD1306_ADDRESS 0x3D
|
||||
#define USE_SH1106
|
||||
#else
|
||||
#define SSD1306_ADDRESS 0x3C
|
||||
#endif
|
||||
#define ST7567_ADDRESS 0x3F
|
||||
|
||||
@@ -210,17 +205,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define INA_ADDR_WAVESHARE_UPS 0x43
|
||||
#define INA3221_ADDR 0x42
|
||||
#define MAX1704X_ADDR 0x36
|
||||
#define QMC6310U_ADDR 0x1C
|
||||
#define QMC6310_ADDR 0x1C
|
||||
#define QMI8658_ADDR 0x6B
|
||||
#define QMC5883L_ADDR 0x0D
|
||||
#define HMC5883L_ADDR 0x1E
|
||||
#define SHTC3_ADDR 0x70
|
||||
#define LPS22HB_ADDR 0x5C
|
||||
#define LPS22HB_ADDR_ALT 0x5D
|
||||
#define SFA30_ADDR 0x5D
|
||||
#define SHT31_4x_ADDR 0x44
|
||||
#define SHT31_4x_ADDR_ALT 0x45
|
||||
#define PMSA003I_ADDR 0x12
|
||||
#define PMSA0031_ADDR 0x12
|
||||
#define QMA6100P_ADDR 0x12
|
||||
#define AHT10_ADDR 0x38
|
||||
#define RCWL9620_ADDR 0x57
|
||||
@@ -234,7 +228,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define NAU7802_ADDR 0x2A
|
||||
#define MAX30102_ADDR 0x57
|
||||
#define SCD4X_ADDR 0x62
|
||||
#define CW2015_ADDR 0x62
|
||||
#define MLX90614_ADDR_DEF 0x5A
|
||||
#define CGRADSENS_ADDR 0x66
|
||||
#define LTR390UV_ADDR 0x53
|
||||
@@ -243,7 +236,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
|
||||
#define BQ25896_ADDR 0x6B
|
||||
#define LTR553ALS_ADDR 0x23
|
||||
#define SEN5X_ADDR 0x69
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ACCELEROMETER
|
||||
@@ -393,6 +385,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#ifndef HAS_RADIO
|
||||
#define HAS_RADIO 0
|
||||
#endif
|
||||
#ifndef HAS_RTC
|
||||
#define HAS_RTC 0
|
||||
#endif
|
||||
#ifndef HAS_CPU_SHUTDOWN
|
||||
#define HAS_CPU_SHUTDOWN 0
|
||||
#endif
|
||||
@@ -428,16 +423,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define HAS_RGB_LED
|
||||
#endif
|
||||
|
||||
#ifndef LED_STATE_OFF
|
||||
#define LED_STATE_OFF 0
|
||||
#endif
|
||||
#ifndef LED_STATE_ON
|
||||
#define LED_STATE_ON 1
|
||||
#endif
|
||||
#ifndef LED_STATE_OFF
|
||||
#define LED_STATE_OFF (LED_STATE_ON ^ 1)
|
||||
#endif
|
||||
|
||||
#ifndef ledOff
|
||||
#define ledOff(pin) pinMode(pin, INPUT)
|
||||
#endif
|
||||
|
||||
// default mapping of pins
|
||||
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)
|
||||
@@ -453,6 +444,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// BME680 BSEC2 support detection
|
||||
#if !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
|
||||
#if defined(RAK_4631) || defined(TBEAM_V10)
|
||||
|
||||
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 1
|
||||
#define MESHTASTIC_BME680_HEADER <bsec2.h>
|
||||
#else
|
||||
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 0
|
||||
#define MESHTASTIC_BME680_HEADER <Adafruit_BME680.h>
|
||||
#endif // defined(RAK_4631)
|
||||
#endif // !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Global switches to turn off features for a minimized build
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -477,7 +480,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_EXCLUDE_AUDIO 1
|
||||
#define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1
|
||||
#define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1
|
||||
#define MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR 1
|
||||
#define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1
|
||||
#define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1
|
||||
#define MESHTASTIC_EXCLUDE_PAXCOUNTER 1
|
||||
|
||||
@@ -43,8 +43,8 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
||||
{
|
||||
ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X, SFA30};
|
||||
return firstOfOrNONE(4, types);
|
||||
ScanI2C::DeviceType types[] = {PMSA0031, SCD4X};
|
||||
return firstOfOrNONE(2, types);
|
||||
}
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstRGBLED() const
|
||||
|
||||
@@ -35,12 +35,11 @@ class ScanI2C
|
||||
SHT4X,
|
||||
SHTC3,
|
||||
LPS22HB,
|
||||
QMC6310U,
|
||||
QMC6310N,
|
||||
QMC6310,
|
||||
QMI8658,
|
||||
QMC5883L,
|
||||
HMC5883L,
|
||||
PMSA003I,
|
||||
PMSA0031,
|
||||
QMA6100P,
|
||||
MPU6050,
|
||||
LIS3DH,
|
||||
@@ -88,10 +87,7 @@ class ScanI2C
|
||||
BH1750,
|
||||
DA217,
|
||||
CHSC6X,
|
||||
CST226SE,
|
||||
SEN5X,
|
||||
SFA30,
|
||||
CW2015
|
||||
CST226SE
|
||||
} DeviceType;
|
||||
|
||||
// typedef uint8_t DeviceAddress;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#include "ScanI2CTwoWire.h"
|
||||
#include "configuration.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
|
||||
@@ -10,7 +8,6 @@
|
||||
#endif
|
||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
|
||||
#include "meshUtils.h" // vformat
|
||||
|
||||
#endif
|
||||
|
||||
bool in_array(uint8_t *array, int size, uint8_t lookfor)
|
||||
@@ -66,10 +63,6 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const
|
||||
if (i2cBus->available()) {
|
||||
r = i2cBus->read();
|
||||
}
|
||||
if (r == 0x80) {
|
||||
LOG_INFO("QMC6310N found at address 0x%02X", addr.address);
|
||||
return ScanI2C::DeviceType::QMC6310N;
|
||||
}
|
||||
r &= 0x0f;
|
||||
|
||||
if (r == 0x08 || r == 0x00) {
|
||||
@@ -113,49 +106,10 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
|
||||
if (i2cBus->available())
|
||||
i2cBus->read();
|
||||
}
|
||||
LOG_DEBUG("Register value from 0x%x: 0x%x", registerLocation.i2cAddress.address, value);
|
||||
LOG_DEBUG("Register value: 0x%x", value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// for SEN5X detection
|
||||
// Note, this code needs to be called before setting the I2C bus speed
|
||||
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
|
||||
// detection will not work
|
||||
String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
|
||||
{
|
||||
uint8_t cmd[] = {0xD0, 0x14};
|
||||
uint8_t response[48] = {0};
|
||||
|
||||
i2cBus->beginTransmission(address);
|
||||
i2cBus->write(cmd, 2);
|
||||
if (i2cBus->endTransmission() != 0)
|
||||
return "";
|
||||
|
||||
delay(20);
|
||||
if (i2cBus->requestFrom(address, (uint8_t)48) != 48)
|
||||
return "";
|
||||
|
||||
for (int i = 0; i < 48 && i2cBus->available(); ++i) {
|
||||
response[i] = i2cBus->read();
|
||||
}
|
||||
|
||||
char productName[33] = {0};
|
||||
int j = 0;
|
||||
for (int i = 0; i < 48 && j < 32; i += 3) {
|
||||
if (response[i] >= 32 && response[i] <= 126)
|
||||
productName[j++] = response[i];
|
||||
else
|
||||
break;
|
||||
|
||||
if (response[i + 1] >= 32 && response[i + 1] <= 126)
|
||||
productName[j++] = response[i + 1];
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
return String(productName);
|
||||
}
|
||||
|
||||
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
||||
case ADDR: \
|
||||
logFoundDevice(__VA_ARGS__); \
|
||||
@@ -221,8 +175,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
type = NONE;
|
||||
if (err == 0) {
|
||||
switch (addr.address) {
|
||||
case SSD1306_ADDRESS_H:
|
||||
case SSD1306_ADDRESS_L:
|
||||
case SSD1306_ADDRESS:
|
||||
type = probeOLED(addr);
|
||||
break;
|
||||
|
||||
@@ -429,11 +382,11 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
}
|
||||
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
|
||||
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
|
||||
if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2);
|
||||
if (registerValue == 0x5449) {
|
||||
type = OPT3001;
|
||||
logFoundDevice("OPT3001", (uint8_t)addr.address);
|
||||
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) !=
|
||||
0) { // unique SHT4x serial number (6 bytes inc. CRC)
|
||||
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number
|
||||
type = SHT4X;
|
||||
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
||||
} else {
|
||||
@@ -458,15 +411,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
break;
|
||||
|
||||
case LPS22HB_ADDR_ALT:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD060), 48); // get device marking
|
||||
if (registerValue != 0) {
|
||||
type = SFA30;
|
||||
logFoundDevice("SFA30", (uint8_t)addr.address);
|
||||
break;
|
||||
}
|
||||
// TODO - What happens with these two?
|
||||
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
|
||||
SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
|
||||
SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310", (uint8_t)addr.address)
|
||||
|
||||
case QMI8658_ADDR:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID
|
||||
@@ -496,7 +442,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
#ifdef HAS_QMA6100P
|
||||
SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address)
|
||||
#else
|
||||
SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address)
|
||||
SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address)
|
||||
#endif
|
||||
case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2);
|
||||
@@ -590,17 +536,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
break;
|
||||
|
||||
SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address);
|
||||
case SCD4X_ADDR: {
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x8), 1);
|
||||
if (registerValue == 0x18) {
|
||||
logFoundDevice("CW2015", (uint8_t)addr.address);
|
||||
type = CW2015;
|
||||
} else {
|
||||
logFoundDevice("SCD4X", (uint8_t)addr.address);
|
||||
type = SCD4X;
|
||||
}
|
||||
break;
|
||||
}
|
||||
SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address);
|
||||
#ifdef HAS_TPS65233
|
||||
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
|
||||
@@ -627,9 +563,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
}
|
||||
break;
|
||||
|
||||
case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
|
||||
case ICM20948_ADDR: // same as BMX160_ADDR
|
||||
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
|
||||
// ICM20948 Register check
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
|
||||
#ifdef HAS_ICM20948
|
||||
type = ICM20948;
|
||||
@@ -640,31 +575,14 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
type = ICM20948;
|
||||
logFoundDevice("ICM20948", (uint8_t)addr.address);
|
||||
break;
|
||||
} else if (addr.address == BMX160_ADDR) {
|
||||
type = BMX160;
|
||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||
break;
|
||||
} else {
|
||||
String prod = "";
|
||||
prod = readSEN5xProductName(i2cBus, addr.address);
|
||||
if (prod.startsWith("SEN55")) {
|
||||
type = SEN5X;
|
||||
logFoundDevice("Sensirion SEN55", addr.address);
|
||||
break;
|
||||
} else if (prod.startsWith("SEN54")) {
|
||||
type = SEN5X;
|
||||
logFoundDevice("Sensirion SEN54", addr.address);
|
||||
break;
|
||||
} else if (prod.startsWith("SEN50")) {
|
||||
type = SEN5X;
|
||||
logFoundDevice("Sensirion SEN50", addr.address);
|
||||
break;
|
||||
}
|
||||
if (addr.address == BMX160_ADDR) {
|
||||
type = BMX160;
|
||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||
break;
|
||||
} else {
|
||||
type = MPU6050;
|
||||
logFoundDevice("MPU6050", (uint8_t)addr.address);
|
||||
break;
|
||||
}
|
||||
type = MPU6050;
|
||||
logFoundDevice("MPU6050", (uint8_t)addr.address);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#include "reClockI2C.h"
|
||||
#include "ScanI2CTwoWire.h"
|
||||
|
||||
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force)
|
||||
{
|
||||
|
||||
uint32_t currentClock = 0;
|
||||
|
||||
/* See https://github.com/arduino/Arduino/issues/11457
|
||||
Currently, only ESP32 can getClock()
|
||||
While all cores can setClock()
|
||||
https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
|
||||
https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
|
||||
https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
|
||||
For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
|
||||
we need to reclock I2C and set it back to the previous desired speed.
|
||||
Only for cases where we can know OR predefine the speed, we can do this.
|
||||
*/
|
||||
|
||||
// TODO add getClock function or return a predefined clock speed per variant?
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
currentClock = i2cBus->getClock();
|
||||
#endif
|
||||
|
||||
if ((currentClock != desiredClock) || force) {
|
||||
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
|
||||
i2cBus->setClock(desiredClock);
|
||||
}
|
||||
|
||||
return currentClock;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
#ifndef RECLOCK_I2C_
|
||||
#define RECLOCK_I2C_
|
||||
|
||||
#include "ScanI2CTwoWire.h"
|
||||
#include <Wire.h>
|
||||
#include <stdint.h>
|
||||
|
||||
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force);
|
||||
|
||||
#endif
|
||||
+8
-11
@@ -896,21 +896,18 @@ void GPS::writePinEN(bool on)
|
||||
void GPS::writePinStandby(bool standby)
|
||||
{
|
||||
#ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones
|
||||
bool val;
|
||||
if (standby)
|
||||
val = GPS_STANDBY_ACTIVE;
|
||||
else
|
||||
val = !GPS_STANDBY_ACTIVE;
|
||||
|
||||
// Determine the new value for the pin
|
||||
// Normally: active HIGH for awake
|
||||
#ifdef PIN_GPS_STANDBY_INVERTED
|
||||
bool val = standby;
|
||||
#else
|
||||
bool val = !standby;
|
||||
#endif
|
||||
|
||||
// Write and log
|
||||
pinMode(PIN_GPS_STANDBY, OUTPUT);
|
||||
digitalWrite(PIN_GPS_STANDBY, val);
|
||||
|
||||
// Enter backup mode on PA1010D; TODO: may be applicable to other MTK GPS too
|
||||
if (IS_ONE_OF(gnssModel, GNSS_MODEL_MTK_PA1010D)) {
|
||||
_serial_gps->write("$PMTK225,4*2F\r\n");
|
||||
}
|
||||
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW");
|
||||
#endif
|
||||
|
||||
@@ -16,11 +16,6 @@
|
||||
#define GPS_EN_ACTIVE 1
|
||||
#endif
|
||||
|
||||
// Allow defining the polarity of the STANDBY output. default is LOW for standby
|
||||
#ifndef GPS_STANDBY_ACTIVE
|
||||
#define GPS_STANDBY_ACTIVE LOW
|
||||
#endif
|
||||
|
||||
static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL;
|
||||
static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000;
|
||||
|
||||
|
||||
+10
-11
@@ -72,13 +72,11 @@ RTCSetResult readFromRTC()
|
||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
||||
#if defined(PCF8563_RTC)
|
||||
if (rtc_found.address == PCF8563_RTC) {
|
||||
SensorPCF8563 rtc;
|
||||
#elif defined(PCF85063_RTC)
|
||||
if (rtc_found.address == PCF85063_RTC) {
|
||||
SensorPCF85063 rtc;
|
||||
|
||||
#endif
|
||||
uint32_t now = millis();
|
||||
SensorRtcHelper rtc;
|
||||
|
||||
#if WIRE_INTERFACES_COUNT == 2
|
||||
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
||||
@@ -242,12 +240,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
||||
#if defined(PCF8563_RTC)
|
||||
if (rtc_found.address == PCF8563_RTC) {
|
||||
SensorPCF8563 rtc;
|
||||
#elif defined(PCF85063_RTC)
|
||||
if (rtc_found.address == PCF85063_RTC) {
|
||||
SensorPCF85063 rtc;
|
||||
|
||||
#endif
|
||||
SensorRtcHelper rtc;
|
||||
|
||||
#if WIRE_INTERFACES_COUNT == 2
|
||||
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
||||
@@ -280,7 +276,11 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
settimeofday(tv, NULL);
|
||||
#endif
|
||||
|
||||
// nrf52 doesn't have a readable RTC (yet - software not written)
|
||||
#if HAS_RTC
|
||||
readFromRTC();
|
||||
#endif
|
||||
|
||||
return RTCSetResultSuccess;
|
||||
} else {
|
||||
return RTCSetResultNotSet; // RTC was already set with a higher quality time
|
||||
@@ -397,7 +397,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local)
|
||||
return (currentQuality >= minQuality) ? getTime(local) : 0;
|
||||
}
|
||||
|
||||
time_t gm_mktime(const struct tm *tm)
|
||||
time_t gm_mktime(struct tm *tm)
|
||||
{
|
||||
#if !MESHTASTIC_EXCLUDE_TZ
|
||||
time_t result = 0;
|
||||
@@ -413,8 +413,8 @@ time_t gm_mktime(const struct tm *tm)
|
||||
days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400);
|
||||
|
||||
// Now, within this tm->year, compute the days *before* this tm->month starts.
|
||||
static const int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
|
||||
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
|
||||
int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
|
||||
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
|
||||
|
||||
// If this is a leap year, and we're past February, add a day:
|
||||
if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) {
|
||||
@@ -435,7 +435,6 @@ time_t gm_mktime(const struct tm *tm)
|
||||
|
||||
return result;
|
||||
#else
|
||||
struct tm tmCopy = *tm;
|
||||
return mktime(&tmCopy);
|
||||
return mktime(tm);
|
||||
#endif
|
||||
}
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false);
|
||||
|
||||
RTCSetResult readFromRTC();
|
||||
|
||||
time_t gm_mktime(const struct tm *tm);
|
||||
time_t gm_mktime(struct tm *tm);
|
||||
|
||||
#define SEC_PER_DAY 86400
|
||||
#define SEC_PER_HOUR 3600
|
||||
|
||||
@@ -9,15 +9,6 @@
|
||||
#include "GxEPD2Multi.h"
|
||||
#endif
|
||||
|
||||
// Limit how often we push a full E-Ink refresh. T-Deck Pro needs faster updates for typing.
|
||||
#ifndef EINK_FORCE_DISPLAY_THROTTLE_MS
|
||||
#if defined(T_DECK_PRO)
|
||||
#define EINK_FORCE_DISPLAY_THROTTLE_MS 200
|
||||
#else
|
||||
#define EINK_FORCE_DISPLAY_THROTTLE_MS 1000
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/**
|
||||
* An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation.
|
||||
*
|
||||
@@ -51,7 +42,7 @@ class EInkDisplay : public OLEDDisplay
|
||||
*
|
||||
* @return true if we did draw the screen
|
||||
*/
|
||||
virtual bool forceDisplay(uint32_t msecLimit = EINK_FORCE_DISPLAY_THROTTLE_MS);
|
||||
virtual bool forceDisplay(uint32_t msecLimit = 1000);
|
||||
|
||||
/**
|
||||
* Run any code needed to complete an update, after the physical refresh has completed.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#ifdef HAS_NEOPIXEL
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
extern Adafruit_NeoPixel pixels;
|
||||
#endif
|
||||
@@ -0,0 +1,5 @@
|
||||
#ifdef HAS_NCP5623
|
||||
#include <NCP5623.h>
|
||||
extern NCP5623 rgb;
|
||||
|
||||
#endif
|
||||
+1
-21
@@ -825,7 +825,7 @@ int32_t Screen::runOnce()
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0 && !suppressRebootBanner) {
|
||||
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
|
||||
showSimpleBanner("Rebooting...", 0);
|
||||
}
|
||||
|
||||
@@ -1731,26 +1731,6 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
showFrame(FrameDirection::PREVIOUS);
|
||||
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||
showFrame(FrameDirection::NEXT);
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F1) {
|
||||
this->ui->switchToFrame(0);
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F2) {
|
||||
this->ui->switchToFrame(1);
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F3) {
|
||||
this->ui->switchToFrame(2);
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F4) {
|
||||
this->ui->switchToFrame(3);
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F5) {
|
||||
this->ui->switchToFrame(4);
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
// Long press up button for fast frame switching
|
||||
showPrevFrame();
|
||||
|
||||
@@ -558,42 +558,6 @@ class Screen : public concurrency::OSThread
|
||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5)
|
||||
return (uint8_t)0;
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(OLED_GR)
|
||||
|
||||
switch (last) {
|
||||
case 0xC3: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)(ch | 0xC0);
|
||||
}
|
||||
// Map UTF-8 Greek chars to Windows-1253 (CP-1253) ASCII codes
|
||||
case 0xCE: {
|
||||
SKIPREST = false;
|
||||
// Uppercase Greek: Α-Ρ (U+0391-U+03A1) -> CP-1253 193-209
|
||||
if (ch >= 145 && ch <= 161)
|
||||
return (uint8_t)(ch + 48);
|
||||
// Uppercase Greek: Σ-Ω (U+03A3-U+03A9) -> CP-1253 211-217
|
||||
else if (ch >= 163 && ch <= 169)
|
||||
return (uint8_t)(ch + 48);
|
||||
// Lowercase Greek: α-ρ (U+03B1-U+03C1) -> CP-1253 225-241
|
||||
else if (ch >= 177 && ch <= 193)
|
||||
return (uint8_t)(ch + 48);
|
||||
break;
|
||||
}
|
||||
case 0xCF: {
|
||||
SKIPREST = false;
|
||||
// Lowercase Greek: ς-ω (U+03C2-U+03C9) -> CP-1253 242-249
|
||||
if (ch >= 130 && ch <= 137)
|
||||
return (uint8_t)(ch + 112);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We want to strip out prefix chars for two-byte Greek char formats
|
||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0xCE || ch == 0xCF)
|
||||
return (uint8_t)0;
|
||||
|
||||
#endif
|
||||
|
||||
// If we already returned an unconvertable-character symbol for this unconvertable-character sequence, return NULs for the
|
||||
|
||||
@@ -16,17 +16,10 @@
|
||||
#include "graphics/fonts/OLEDDisplayFontsCS.h"
|
||||
#endif
|
||||
|
||||
#ifdef OLED_GR
|
||||
#include "graphics/fonts/OLEDDisplayFontsGR.h"
|
||||
#endif
|
||||
|
||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
|
||||
#include "graphics/fonts/EinkDisplayFonts.h"
|
||||
#endif
|
||||
|
||||
#ifdef OLED_GR
|
||||
#define FONT_SMALL_LOCAL ArialMT_Plain_10_GR // Height: 13
|
||||
#else
|
||||
#ifdef OLED_PL
|
||||
#define FONT_SMALL_LOCAL ArialMT_Plain_10_PL
|
||||
#else
|
||||
@@ -44,10 +37,6 @@
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#ifdef OLED_GR
|
||||
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_GR // Height: 19
|
||||
#else
|
||||
#ifdef OLED_PL
|
||||
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19
|
||||
#else
|
||||
@@ -65,10 +54,6 @@
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#ifdef OLED_GR
|
||||
#define FONT_LARGE_LOCAL ArialMT_Plain_24_GR // Height: 28
|
||||
#else
|
||||
#ifdef OLED_PL
|
||||
#define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28
|
||||
#else
|
||||
@@ -86,7 +71,6 @@
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
|
||||
@@ -221,6 +221,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
|
||||
if (rtc_sec > 0) {
|
||||
// === Build Time String ===
|
||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
int hour, minute, second;
|
||||
graphics::decomposeTime(rtc_sec, hour, minute, second);
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||
@@ -427,33 +428,39 @@ const int *getTextPositions(OLEDDisplay *display)
|
||||
// *************************
|
||||
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
{
|
||||
if (!isAPIConnected(service->api_state))
|
||||
return;
|
||||
bool drawConnectionState = false;
|
||||
if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI ||
|
||||
service->api_state == service->STATE_SERIAL || service->api_state == service->STATE_PACKET ||
|
||||
service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) {
|
||||
drawConnectionState = true;
|
||||
}
|
||||
|
||||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||||
(connection_icon_height * scale) + (2 * scale));
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
int iconX = 0;
|
||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||
if (drawConnectionState) {
|
||||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||||
(connection_icon_height * scale) + (2 * scale));
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
int iconX = 0;
|
||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||
|
||||
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
||||
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
||||
for (int xx = 0; xx < connection_icon_width; ++xx) {
|
||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
||||
if (byteVal & bitMask) {
|
||||
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
|
||||
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
||||
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
||||
for (int xx = 0; xx < connection_icon_width; ++xx) {
|
||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
||||
if (byteVal & bitMask) {
|
||||
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
|
||||
connection_icon);
|
||||
} else {
|
||||
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
|
||||
connection_icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,18 +63,4 @@ bool isAllowedPunctuation(char c);
|
||||
|
||||
std::string sanitizeString(const std::string &input);
|
||||
|
||||
static inline bool isAPIConnected(uint8_t state)
|
||||
{
|
||||
static constexpr bool connectedStates[] = {
|
||||
/* STATE_NONE */ false,
|
||||
/* STATE_BLE */ true,
|
||||
/* STATE_WIFI */ true,
|
||||
/* STATE_SERIAL */ true,
|
||||
/* STATE_PACKET */ true,
|
||||
/* STATE_HTTP */ true,
|
||||
/* STATE_ETH */ true,
|
||||
};
|
||||
return state < sizeof(connectedStates) ? connectedStates[state] : false;
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
@@ -110,14 +110,14 @@ void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, ui
|
||||
uint32_t secs = (uptimeMillis % 60000) / 1000;
|
||||
|
||||
if (days) {
|
||||
snprintf(uptimeStr, maxLength, "%s%ud %uh", prefix, days, hours);
|
||||
snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours);
|
||||
} else if (hours) {
|
||||
snprintf(uptimeStr, maxLength, "%s%uh %um", prefix, hours, mins);
|
||||
snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins);
|
||||
} else if (!includeSecs) {
|
||||
snprintf(uptimeStr, maxLength, "%s%um", prefix, mins);
|
||||
snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins);
|
||||
} else if (mins) {
|
||||
snprintf(uptimeStr, maxLength, "%s%um %us", prefix, mins, secs);
|
||||
snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs);
|
||||
} else {
|
||||
snprintf(uptimeStr, maxLength, "%s%us", prefix, secs);
|
||||
snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,10 +429,6 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
|
||||
c = c - 'a' + 'A';
|
||||
}
|
||||
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
|
||||
// Show the common "/" pairing next to "?" like on a real keyboard
|
||||
if (key.type == VK_CHAR && key.character == '?') {
|
||||
keyText = "?/";
|
||||
}
|
||||
}
|
||||
|
||||
int textWidth = display->getStringWidth(keyText.c_str());
|
||||
@@ -522,13 +518,9 @@ char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
|
||||
|
||||
char c = key.character;
|
||||
|
||||
// Long-press: letters become uppercase; for "?" provide "/" like a typical keyboard
|
||||
if (isLongPress) {
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c = (char)(c - 'a' + 'A');
|
||||
} else if (c == '?') {
|
||||
c = '/';
|
||||
}
|
||||
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
|
||||
if (isLongPress && c >= 'a' && c <= 'z') {
|
||||
c = (char)(c - 'a' + 'A');
|
||||
}
|
||||
|
||||
return c;
|
||||
|
||||
@@ -438,7 +438,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
|
||||
} else {
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz (%d)", freqStr, config.lora.channel_num);
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
|
||||
}
|
||||
}
|
||||
size_t len = strlen(frequencyslot);
|
||||
@@ -663,7 +663,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
|
||||
if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it
|
||||
char uptimeStr[32] = "";
|
||||
getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr));
|
||||
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
|
||||
textWidth = display->getStringWidth(uptimeStr);
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line++], uptimeStr);
|
||||
|
||||
+191
-410
File diff suppressed because it is too large
Load Diff
@@ -8,70 +8,65 @@ class menuHandler
|
||||
{
|
||||
public:
|
||||
enum screenMenus {
|
||||
MenuNone,
|
||||
LoraMenu,
|
||||
LoraPicker,
|
||||
DeviceRolePicker,
|
||||
RadioPresetPicker,
|
||||
FrequencySlot,
|
||||
NoTimeoutLoraPicker,
|
||||
TzPicker,
|
||||
TwelveHourPicker,
|
||||
ClockFacePicker,
|
||||
ClockMenu,
|
||||
PositionBaseMenu,
|
||||
NodeBaseMenu,
|
||||
GpsToggleMenu,
|
||||
GpsFormatMenu,
|
||||
GpsSmartPositionMenu,
|
||||
GpsUpdateIntervalMenu,
|
||||
GpsPositionBroadcastMenu,
|
||||
CompassPointNorthMenu,
|
||||
ResetNodeDbMenu,
|
||||
BuzzerModeMenuPicker,
|
||||
MuiPicker,
|
||||
TftColorMenuPicker,
|
||||
BrightnessPicker,
|
||||
RebootMenu,
|
||||
ShutdownMenu,
|
||||
NodePickerMenu,
|
||||
ManageNodeMenu,
|
||||
RemoveFavorite,
|
||||
TestMenu,
|
||||
NumberTest,
|
||||
WifiToggleMenu,
|
||||
BluetoothToggleMenu,
|
||||
ScreenOptionsMenu,
|
||||
PowerMenu,
|
||||
SystemBaseMenu,
|
||||
KeyVerificationInit,
|
||||
KeyVerificationFinalPrompt,
|
||||
TraceRouteMenu,
|
||||
ThrottleMessage,
|
||||
MessageResponseMenu,
|
||||
MessageViewModeMenu,
|
||||
ReplyMenu,
|
||||
DeleteMessagesMenu,
|
||||
NodeNameLengthMenu,
|
||||
menu_none,
|
||||
lora_Menu,
|
||||
lora_picker,
|
||||
device_role_picker,
|
||||
radio_preset_picker,
|
||||
no_timeout_lora_picker,
|
||||
TZ_picker,
|
||||
twelve_hour_picker,
|
||||
clock_face_picker,
|
||||
clock_menu,
|
||||
position_base_menu,
|
||||
node_base_menu,
|
||||
gps_toggle_menu,
|
||||
gps_format_menu,
|
||||
gps_smart_position_menu,
|
||||
gps_update_interval_menu,
|
||||
gps_position_broadcast_menu,
|
||||
compass_point_north_menu,
|
||||
reset_node_db_menu,
|
||||
buzzermodemenupicker,
|
||||
mui_picker,
|
||||
tftcolormenupicker,
|
||||
brightness_picker,
|
||||
reboot_menu,
|
||||
shutdown_menu,
|
||||
add_favorite,
|
||||
remove_favorite,
|
||||
test_menu,
|
||||
number_test,
|
||||
wifi_toggle_menu,
|
||||
bluetooth_toggle_menu,
|
||||
screen_options_menu,
|
||||
power_menu,
|
||||
system_base_menu,
|
||||
key_verification_init,
|
||||
key_verification_final_prompt,
|
||||
trace_route_menu,
|
||||
throttle_message,
|
||||
message_response_menu,
|
||||
message_viewmode_menu,
|
||||
reply_menu,
|
||||
delete_messages_menu,
|
||||
node_name_length_menu,
|
||||
FrameToggles,
|
||||
DisplayUnits,
|
||||
MessageBubblesMenu
|
||||
DisplayUnits
|
||||
};
|
||||
static screenMenus menuQueue;
|
||||
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
||||
|
||||
static void OnboardMessage();
|
||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||
static void loraMenu();
|
||||
static void deviceRolePicker();
|
||||
static void radioPresetPicker();
|
||||
static void FrequencySlotPicker();
|
||||
static void DeviceRolePicker();
|
||||
static void RadioPresetPicker();
|
||||
static void handleMenuSwitch(OLEDDisplay *display);
|
||||
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
||||
static void clockMenu();
|
||||
static void TZPicker();
|
||||
static void twelveHourPicker();
|
||||
static void clockFacePicker();
|
||||
static void TwelveHourPicker();
|
||||
static void ClockFacePicker();
|
||||
static void messageResponseMenu();
|
||||
static void messageViewModeMenu();
|
||||
static void replyMenu();
|
||||
@@ -95,8 +90,6 @@ class menuHandler
|
||||
static void BrightnessPickerMenu();
|
||||
static void rebootMenu();
|
||||
static void shutdownMenu();
|
||||
static void NodePicker();
|
||||
static void manageNodeMenu();
|
||||
static void addFavoriteMenu();
|
||||
static void removeFavoriteMenu();
|
||||
static void traceRouteMenu();
|
||||
@@ -107,16 +100,15 @@ class menuHandler
|
||||
static void screenOptionsMenu();
|
||||
static void powerMenu();
|
||||
static void nodeNameLengthMenu();
|
||||
static void frameTogglesMenu();
|
||||
static void displayUnitsMenu();
|
||||
static void messageBubblesMenu();
|
||||
static void FrameToggles_menu();
|
||||
static void DisplayUnits_menu();
|
||||
static void textMessageMenu();
|
||||
|
||||
private:
|
||||
static void saveUIConfig();
|
||||
static void keyVerificationInitMenu();
|
||||
static void keyVerificationFinalPrompt();
|
||||
static void bluetoothToggleMenu();
|
||||
static void BluetoothToggleMenu();
|
||||
};
|
||||
|
||||
/* Generic Menu Options designations */
|
||||
@@ -142,7 +134,7 @@ struct ScreenColor {
|
||||
uint8_t b;
|
||||
bool useVariant;
|
||||
|
||||
explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
|
||||
ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
|
||||
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
|
||||
{
|
||||
}
|
||||
@@ -157,7 +149,6 @@ using GPSToggleOption = MenuOption<meshtastic_Config_PositionConfig_GpsMode>;
|
||||
using GPSFormatOption = MenuOption<meshtastic_DeviceUIConfig_GpsCoordinateFormat>;
|
||||
using NodeNameOption = MenuOption<bool>;
|
||||
using PositionMenuOption = MenuOption<int>;
|
||||
using ManageNodeOption = MenuOption<int>;
|
||||
using ClockFaceOption = MenuOption<bool>;
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
// External declarations
|
||||
extern bool hasUnreadMessage;
|
||||
extern meshtastic_DeviceState devicestate;
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
using graphics::Emote;
|
||||
@@ -47,7 +49,7 @@ static inline size_t utf8CharLen(uint8_t c)
|
||||
}
|
||||
|
||||
// Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels
|
||||
static std::string normalizeEmoji(const std::string &s)
|
||||
std::string normalizeEmoji(const std::string &s)
|
||||
{
|
||||
std::string out;
|
||||
for (size_t i = 0; i < s.size();) {
|
||||
@@ -80,7 +82,6 @@ uint32_t pauseStart = 0;
|
||||
bool waitingToReset = false;
|
||||
bool scrollStarted = false;
|
||||
static bool didReset = false;
|
||||
static constexpr int MESSAGE_BLOCK_GAP = 6;
|
||||
|
||||
void scrollUp()
|
||||
{
|
||||
@@ -110,6 +111,22 @@ void scrollDown()
|
||||
|
||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
||||
{
|
||||
std::string renderLine;
|
||||
for (size_t i = 0; i < line.size();) {
|
||||
uint8_t c = (uint8_t)line[i];
|
||||
size_t len = utf8CharLen(c);
|
||||
if (c == 0xEF && i + 2 < line.size() && (uint8_t)line[i + 1] == 0xB8 && (uint8_t)line[i + 2] == 0x8F) {
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
if (c == 0xF0 && i + 3 < line.size() && (uint8_t)line[i + 1] == 0x9F && (uint8_t)line[i + 2] == 0x8F &&
|
||||
((uint8_t)line[i + 3] >= 0xBB && (uint8_t)line[i + 3] <= 0xBF)) {
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
renderLine.append(line, i, len);
|
||||
i += len;
|
||||
}
|
||||
int cursorX = x;
|
||||
const int fontHeight = FONT_HEIGHT_SMALL;
|
||||
|
||||
@@ -186,7 +203,8 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
|
||||
|
||||
// Render the emote (if found)
|
||||
if (matchedEmote && i == nextEmotePos) {
|
||||
int iconY = y + (lineHeight - matchedEmote->height) / 2;
|
||||
// Vertically center emote relative to font baseline (not just midline)
|
||||
int iconY = fontY + (fontHeight - matchedEmote->height) / 2;
|
||||
display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap);
|
||||
cursorX += matchedEmote->width + 1;
|
||||
i += emojiLen;
|
||||
@@ -405,63 +423,6 @@ static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &
|
||||
return totalWidth;
|
||||
}
|
||||
|
||||
struct MessageBlock {
|
||||
size_t start;
|
||||
size_t end;
|
||||
bool mine;
|
||||
};
|
||||
|
||||
static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine)
|
||||
{
|
||||
if (isHeaderLine) {
|
||||
return lineTopY + (FONT_HEIGHT_SMALL - 1);
|
||||
}
|
||||
|
||||
int tallest = FONT_HEIGHT_SMALL;
|
||||
for (int e = 0; e < numEmotes; ++e) {
|
||||
if (line.find(emotes[e].label) != std::string::npos) {
|
||||
if (emotes[e].height > tallest)
|
||||
tallest = emotes[e].height;
|
||||
}
|
||||
}
|
||||
|
||||
const int lineHeight = std::max(FONT_HEIGHT_SMALL, tallest);
|
||||
const int iconTop = lineTopY + (lineHeight - tallest) / 2;
|
||||
|
||||
return iconTop + tallest - 1;
|
||||
}
|
||||
|
||||
static std::vector<MessageBlock> buildMessageBlocks(const std::vector<bool> &isHeaderVec, const std::vector<bool> &isMineVec)
|
||||
{
|
||||
std::vector<MessageBlock> blocks;
|
||||
if (isHeaderVec.empty())
|
||||
return blocks;
|
||||
|
||||
size_t start = 0;
|
||||
bool mine = isMineVec[0];
|
||||
|
||||
for (size_t i = 1; i < isHeaderVec.size(); ++i) {
|
||||
if (isHeaderVec[i]) {
|
||||
MessageBlock b;
|
||||
b.start = start;
|
||||
b.end = i - 1;
|
||||
b.mine = mine;
|
||||
blocks.push_back(b);
|
||||
|
||||
start = i;
|
||||
mine = isMineVec[i];
|
||||
}
|
||||
}
|
||||
|
||||
MessageBlock last;
|
||||
last.start = start;
|
||||
last.end = isHeaderVec.size() - 1;
|
||||
last.mine = mine;
|
||||
blocks.push_back(last);
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY)
|
||||
{
|
||||
if (totalHeight <= visibleHeight)
|
||||
@@ -521,18 +482,9 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
constexpr int LEFT_MARGIN = 2;
|
||||
constexpr int RIGHT_MARGIN = 2;
|
||||
constexpr int SCROLLBAR_WIDTH = 3;
|
||||
constexpr int BUBBLE_PAD_X = 3;
|
||||
constexpr int BUBBLE_PAD_Y = 4;
|
||||
constexpr int BUBBLE_RADIUS = 4;
|
||||
constexpr int BUBBLE_MIN_W = 24;
|
||||
constexpr int BUBBLE_TEXT_INDENT = 2;
|
||||
|
||||
// Check if bubbles are enabled
|
||||
const bool showBubbles = config.display.enable_message_bubbles;
|
||||
const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN;
|
||||
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN;
|
||||
|
||||
// Derived widths
|
||||
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0);
|
||||
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
|
||||
|
||||
// Title string depending on mode
|
||||
@@ -595,28 +547,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
char chanType[32] = "";
|
||||
if (currentMode == ThreadMode::ALL) {
|
||||
if (m.dest == NODENUM_BROADCAST) {
|
||||
const char *name = channels.getName(m.channelIndex);
|
||||
if (currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) {
|
||||
if (strcmp(name, "ShortTurbo") == 0)
|
||||
name = "ShortT";
|
||||
else if (strcmp(name, "ShortSlow") == 0)
|
||||
name = "ShortS";
|
||||
else if (strcmp(name, "ShortFast") == 0)
|
||||
name = "ShortF";
|
||||
else if (strcmp(name, "MediumSlow") == 0)
|
||||
name = "MedS";
|
||||
else if (strcmp(name, "MediumFast") == 0)
|
||||
name = "MedF";
|
||||
else if (strcmp(name, "LongSlow") == 0)
|
||||
name = "LongS";
|
||||
else if (strcmp(name, "LongFast") == 0)
|
||||
name = "LongF";
|
||||
else if (strcmp(name, "LongTurbo") == 0)
|
||||
name = "LongT";
|
||||
else if (strcmp(name, "LongMod") == 0)
|
||||
name = "LongM";
|
||||
}
|
||||
snprintf(chanType, sizeof(chanType), "#%s", name);
|
||||
snprintf(chanType, sizeof(chanType), "#%s", channels.getName(m.channelIndex));
|
||||
} else {
|
||||
snprintf(chanType, sizeof(chanType), "(DM)");
|
||||
}
|
||||
@@ -683,8 +614,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
}
|
||||
|
||||
// Shrink Sender name if needed
|
||||
int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) -
|
||||
display->getStringWidth(chanType) - display->getStringWidth(" @...");
|
||||
int availWidth = SCREEN_WIDTH - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) -
|
||||
display->getStringWidth(" @...") - 10;
|
||||
if (availWidth < 0)
|
||||
availWidth = 0;
|
||||
|
||||
@@ -736,8 +667,6 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
cachedLines = allLines;
|
||||
cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader);
|
||||
|
||||
std::vector<MessageBlock> blocks = buildMessageBlocks(isHeader, isMine);
|
||||
|
||||
// Scrolling logic (unchanged)
|
||||
int totalHeight = 0;
|
||||
for (size_t i = 0; i < cachedHeights.size(); ++i)
|
||||
@@ -785,124 +714,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
|
||||
int finalScroll = (int)scrollY;
|
||||
int yOffset = -finalScroll + getTextPositions(display)[1];
|
||||
const int contentTop = getTextPositions(display)[1];
|
||||
const int contentBottom = scrollBottom; // already excludes nav line
|
||||
const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
||||
const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2);
|
||||
|
||||
std::vector<int> lineTop;
|
||||
lineTop.resize(cachedLines.size());
|
||||
{
|
||||
int acc = 0;
|
||||
for (size_t i = 0; i < cachedLines.size(); ++i) {
|
||||
lineTop[i] = yOffset + acc;
|
||||
acc += cachedHeights[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Draw bubbles (only if enabled)
|
||||
if (showBubbles) {
|
||||
for (size_t bi = 0; bi < blocks.size(); ++bi) {
|
||||
const auto &b = blocks[bi];
|
||||
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
|
||||
continue;
|
||||
|
||||
int visualTop = lineTop[b.start];
|
||||
|
||||
int topY;
|
||||
if (isHeader[b.start]) {
|
||||
// Header start
|
||||
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
|
||||
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
|
||||
} else {
|
||||
// Body start
|
||||
bool thisLineHasEmote = false;
|
||||
for (int e = 0; e < numEmotes; ++e) {
|
||||
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
|
||||
thisLineHasEmote = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (thisLineHasEmote) {
|
||||
constexpr int EMOTE_PADDING_ABOVE = 4;
|
||||
visualTop -= EMOTE_PADDING_ABOVE;
|
||||
}
|
||||
topY = visualTop - BUBBLE_PAD_Y;
|
||||
}
|
||||
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
|
||||
int bottomY = visualBottom + BUBBLE_PAD_Y;
|
||||
|
||||
if (bi + 1 < blocks.size()) {
|
||||
int nextHeaderIndex = (int)blocks[bi + 1].start;
|
||||
int nextTop = lineTop[nextHeaderIndex];
|
||||
int maxBottom = nextTop - 1 - bubbleGapY;
|
||||
if (bottomY > maxBottom)
|
||||
bottomY = maxBottom;
|
||||
}
|
||||
|
||||
if (bottomY <= topY + 2)
|
||||
continue;
|
||||
|
||||
if (bottomY < contentTop || topY > contentBottom - 1)
|
||||
continue;
|
||||
|
||||
int maxLineW = 0;
|
||||
|
||||
for (size_t i = b.start; i <= b.end; ++i) {
|
||||
int w = 0;
|
||||
if (isHeader[i]) {
|
||||
w = display->getStringWidth(cachedLines[i].c_str());
|
||||
if (b.mine)
|
||||
w += 12; // room for ACK/NACK/relay mark
|
||||
} else {
|
||||
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
||||
}
|
||||
if (w > maxLineW)
|
||||
maxLineW = w;
|
||||
}
|
||||
|
||||
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2));
|
||||
int bubbleH = (bottomY - topY) + 1;
|
||||
int bubbleX = 0;
|
||||
if (b.mine) {
|
||||
bubbleX = rightEdge - bubbleW;
|
||||
} else {
|
||||
bubbleX = x;
|
||||
}
|
||||
if (bubbleX < x)
|
||||
bubbleX = x;
|
||||
if (bubbleX + bubbleW > rightEdge)
|
||||
bubbleW = std::max(1, rightEdge - bubbleX);
|
||||
|
||||
// Draw rounded rectangle bubble
|
||||
if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) {
|
||||
const int r = BUBBLE_RADIUS;
|
||||
const int bx = bubbleX;
|
||||
const int by = topY;
|
||||
const int bw = bubbleW;
|
||||
const int bh = bubbleH;
|
||||
|
||||
// Draw the 4 corner arcs using drawCircleQuads
|
||||
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
|
||||
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
|
||||
|
||||
// Draw the 4 edges between corners
|
||||
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
|
||||
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
|
||||
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
|
||||
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
|
||||
} else if (bubbleW > 1 && bubbleH > 1) {
|
||||
// Fallback to simple rectangle for very small bubbles
|
||||
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
|
||||
}
|
||||
}
|
||||
} // end if (showBubbles)
|
||||
|
||||
// Render visible lines
|
||||
int lineY = yOffset;
|
||||
for (size_t i = 0; i < cachedLines.size(); ++i) {
|
||||
int lineY = yOffset;
|
||||
for (size_t j = 0; j < i; ++j)
|
||||
lineY += cachedHeights[j];
|
||||
|
||||
if (lineY > -cachedHeights[i] && lineY < scrollBottom) {
|
||||
if (isHeader[i]) {
|
||||
@@ -911,28 +728,14 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
int headerX;
|
||||
if (isMine[i]) {
|
||||
// push header left to avoid overlap with scrollbar
|
||||
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - (showBubbles ? textIndent : 0);
|
||||
headerX = SCREEN_WIDTH - w - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
||||
if (headerX < LEFT_MARGIN)
|
||||
headerX = LEFT_MARGIN;
|
||||
} else {
|
||||
headerX = x + textIndent;
|
||||
headerX = x;
|
||||
}
|
||||
display->drawString(headerX, lineY, cachedLines[i].c_str());
|
||||
|
||||
// Draw underline just under header text
|
||||
int underlineY = lineY + FONT_HEIGHT_SMALL;
|
||||
|
||||
int underlineW = w;
|
||||
int maxW = rightEdge - headerX;
|
||||
if (maxW < 0)
|
||||
maxW = 0;
|
||||
if (underlineW > maxW)
|
||||
underlineW = maxW;
|
||||
|
||||
for (int px = 0; px < underlineW; ++px) {
|
||||
display->setPixel(headerX + px, underlineY);
|
||||
}
|
||||
|
||||
// Draw ACK/NACK mark for our own messages
|
||||
if (isMine[i]) {
|
||||
int markX = headerX - 10;
|
||||
@@ -950,27 +753,32 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
// AckStatus::NONE → show nothing
|
||||
}
|
||||
|
||||
// Draw underline just under header text
|
||||
int underlineY = lineY + FONT_HEIGHT_SMALL;
|
||||
for (int px = 0; px < w; ++px) {
|
||||
display->setPixel(headerX + px, underlineY);
|
||||
}
|
||||
} else {
|
||||
// Render message line
|
||||
if (isMine[i]) {
|
||||
// Calculate actual rendered width including emotes
|
||||
int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
||||
int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - (showBubbles ? textIndent : 0);
|
||||
int rightX = SCREEN_WIDTH - renderedWidth - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
||||
if (rightX < LEFT_MARGIN)
|
||||
rightX = LEFT_MARGIN;
|
||||
|
||||
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
|
||||
} else {
|
||||
drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes);
|
||||
drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lineY += cachedHeights[i];
|
||||
}
|
||||
int totalContentHeight = totalHeight;
|
||||
int visibleHeight = usableHeight;
|
||||
|
||||
// Draw scrollbar
|
||||
drawMessageScrollbar(display, usableHeight, totalHeight, finalScroll, getTextPositions(display)[1]);
|
||||
drawMessageScrollbar(display, visibleHeight, totalContentHeight, finalScroll, getTextPositions(display)[1]);
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
}
|
||||
@@ -1033,6 +841,7 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
||||
constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line
|
||||
constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn)
|
||||
constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines
|
||||
constexpr int MESSAGE_BLOCK_GAP = 4; // gap after a message block before a new header
|
||||
constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above)
|
||||
constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line)
|
||||
|
||||
@@ -1042,7 +851,6 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
||||
for (size_t idx = 0; idx < lines.size(); ++idx) {
|
||||
const auto &line = lines[idx];
|
||||
const int baseHeight = FONT_HEIGHT_SMALL;
|
||||
int lineHeight = baseHeight;
|
||||
|
||||
// Detect if THIS line or NEXT line contains an emote
|
||||
bool hasEmote = false;
|
||||
@@ -1064,6 +872,8 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
||||
}
|
||||
}
|
||||
|
||||
int lineHeight = baseHeight;
|
||||
|
||||
if (isHeaderVec[idx]) {
|
||||
// Header line spacing
|
||||
lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP;
|
||||
@@ -1112,7 +922,7 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
||||
|
||||
// Banner logic
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
|
||||
char longName[48] = "?";
|
||||
char longName[48] = "???";
|
||||
if (node && node->user.long_name) {
|
||||
strncpy(longName, node->user.long_name, sizeof(longName) - 1);
|
||||
longName[sizeof(longName) - 1] = '\0';
|
||||
|
||||
@@ -176,7 +176,6 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
|
||||
|
||||
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
|
||||
{
|
||||
x = (currentResolution == ScreenResolution::High) ? x - 2 : (currentResolution == ScreenResolution::Low) ? x - 1 : x;
|
||||
for (int y = yStart; y <= yEnd; y += 2) {
|
||||
display->setPixel(x, y);
|
||||
}
|
||||
@@ -206,11 +205,9 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
||||
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
{
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int nameMaxWidth = columnWidth - 25;
|
||||
int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
|
||||
char timeStr[10];
|
||||
uint32_t seconds = sinceLastSeen(node);
|
||||
@@ -237,13 +234,6 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
|
||||
}
|
||||
}
|
||||
|
||||
int rightEdge = x + columnWidth - timeOffset;
|
||||
if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time
|
||||
@@ -263,7 +253,6 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
int barsXOffset = columnWidth - barsOffset;
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
@@ -276,13 +265,6 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw signal strength bars
|
||||
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
||||
@@ -316,7 +298,6 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
char distStr[10] = "";
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
@@ -377,13 +358,6 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen(distStr) > 0) {
|
||||
int offset = (currentResolution == ScreenResolution::High)
|
||||
@@ -418,7 +392,6 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
@@ -430,13 +403,6 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user