Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80fcb061e6 | ||
|
|
334ad9b313 | ||
|
|
0953706e9e | ||
|
|
309d51a3e8 | ||
|
|
93f87c57b9 | ||
|
|
94ef2ae451 | ||
|
|
abef0d85a2 | ||
|
|
6b3f975ba5 |
@@ -0,0 +1,160 @@
|
||||
name: Post Web Flasher Link Comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
post-flasher-link:
|
||||
if: >
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion != 'cancelled' &&
|
||||
github.repository == 'meshtastic/firmware'
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Post or update web flasher link comment
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- web-flasher-link -->';
|
||||
const run = context.payload.workflow_run;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
// Resolve the PR number (run.pull_requests is empty for fork PRs)
|
||||
let prNumber = run.pull_requests?.[0]?.number;
|
||||
if (!prNumber) {
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner, repo, commit_sha: run.head_sha,
|
||||
});
|
||||
prNumber = (prs.find((pr) => pr.head.sha === run.head_sha) ?? prs[0])?.number;
|
||||
}
|
||||
if (!prNumber) {
|
||||
core.info('No pull request associated with this run; skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||
|
||||
// Only comment on PRs authored by members of the organization.
|
||||
// author_association MEMBER is computed by GitHub and reflects org
|
||||
// membership (including concealed members); OWNER covers a repo owner.
|
||||
const allowedAssociations = ['OWNER', 'MEMBER'];
|
||||
if (!allowedAssociations.includes(pr.author_association)) {
|
||||
core.info(`Author association ${pr.author_association} is not an org member; skipping.`);
|
||||
return;
|
||||
}
|
||||
if (pr.state !== 'open') {
|
||||
core.info('Pull request is not open; skipping.');
|
||||
return;
|
||||
}
|
||||
if (pr.head.sha !== run.head_sha) {
|
||||
core.info('Run is for an outdated commit; skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Require at least one per-arch firmware artifact from gather-artifacts
|
||||
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
|
||||
owner, repo, run_id: run.id, per_page: 100,
|
||||
});
|
||||
const archRe = /^firmware-(esp32|esp32s3|esp32c3|esp32c6|nrf52840|rp2040|rp2350|stm32)-(\d+\.\d+\.\d+\.[0-9a-f]+)$/;
|
||||
const archArtifacts = artifacts.filter((a) => archRe.test(a.name) && !a.expired);
|
||||
if (archArtifacts.length === 0) {
|
||||
core.info('No per-arch firmware artifacts found; skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
const version = archRe.exec(archArtifacts[0].name)[2];
|
||||
const expiresAt = archArtifacts[0].expires_at
|
||||
? new Date(archArtifacts[0].expires_at).toISOString().slice(0, 10)
|
||||
: null;
|
||||
|
||||
// Per-board deep links from manifest-{platform}-{board}-{version} artifacts
|
||||
const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const boardRe = new RegExp(`^manifest-([a-z0-9]+)-(.+)-${escapedVersion}$`);
|
||||
let boards = artifacts
|
||||
.map((a) => boardRe.exec(a.name))
|
||||
.filter(Boolean)
|
||||
.map((m) => ({ platform: m[1], board: m[2] }))
|
||||
.sort((a, b) => a.board.localeCompare(b.board));
|
||||
|
||||
// Limit the list to devices the web flasher actively supports — the same
|
||||
// activelySupported / non-portduino filter the flasher applies to its own
|
||||
// device list — matching variant envs (-tft/-inkhud) to their base target.
|
||||
// On a fetch failure, fall back to listing all built boards.
|
||||
try {
|
||||
const hw = await (await fetch('https://api.meshtastic.org/resource/deviceHardware')).json();
|
||||
const supported = new Set(
|
||||
hw.filter((d) => d.activelySupported && !String(d.architecture || '').startsWith('portduino'))
|
||||
.map((d) => d.platformioTarget),
|
||||
);
|
||||
const baseTarget = (b) => b.replace(/-(tft|inkhud)$/, '');
|
||||
boards = boards.filter((b) => supported.has(b.board) || supported.has(baseTarget(b.board)));
|
||||
} catch (e) {
|
||||
core.warning(`Could not fetch device hardware list; listing all built boards. ${e.message}`);
|
||||
}
|
||||
|
||||
const flasherUrl = `https://flasher.meshtastic.org/?pr=${prNumber}`;
|
||||
const boardLines = boards
|
||||
.map((b) => `| [\`${b.board}\`](${flasherUrl}&device=${encodeURIComponent(b.board)}) | ${b.platform} |`)
|
||||
.join('\n');
|
||||
|
||||
// Shields.io badges. Only non-user-controlled, charset-constrained values
|
||||
// (version, commit sha, counts, dates) go into badge URLs — never board
|
||||
// names or the PR title — so the rendered comment cannot be spoofed.
|
||||
const shieldText = (s) =>
|
||||
encodeURIComponent(String(s).replace(/-/g, '--').replace(/_/g, '__').replace(/ /g, '_'));
|
||||
const shield = (label, message, color) =>
|
||||
`https://img.shields.io/badge/${shieldText(label)}-${shieldText(message)}-${color}`;
|
||||
const buttonUrl =
|
||||
`https://img.shields.io/badge/${shieldText('Flash this PR in the Web Flasher')}-2C2D3C?style=for-the-badge`;
|
||||
const badges = [
|
||||
`})`,
|
||||
`, '2C2D3C')})`,
|
||||
`})`,
|
||||
];
|
||||
if (expiresAt) badges.push(`})`);
|
||||
|
||||
// Only render the board table when there are supported boards to list
|
||||
const boardTable = boards.length > 0 ? [
|
||||
`<details><summary>Supported boards built by this PR (${boards.length})</summary>`,
|
||||
'',
|
||||
'| Board | Platform |',
|
||||
'| --- | --- |',
|
||||
boardLines,
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
] : [];
|
||||
|
||||
const body = [
|
||||
marker,
|
||||
'## 🔦 Try this PR in the Web Flasher',
|
||||
'',
|
||||
`[](${flasherUrl})`,
|
||||
'',
|
||||
badges.join(' '),
|
||||
'',
|
||||
'> [!WARNING]',
|
||||
'> This is an automated, unreviewed CI test build. Back up your device configuration',
|
||||
'> before flashing, and only flash devices you are able to recover.',
|
||||
'',
|
||||
...boardTable,
|
||||
`*Build artifacts expire${expiresAt ? ` on ${expiresAt}` : ' after 30 days'}. Updated for \`${run.head_sha.slice(0, 7)}\`.*`,
|
||||
].join('\n');
|
||||
|
||||
// Sticky comment: update in place when the marker is found
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number: prNumber, per_page: 100,
|
||||
});
|
||||
const existing = comments.find((c) => c.body?.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
* For more information, see: https://meshtastic.org/
|
||||
*/
|
||||
#include "power.h"
|
||||
#include "BluetoothCommon.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
@@ -962,6 +963,10 @@ void Power::readPowerStatus()
|
||||
lastLogTime = millis();
|
||||
}
|
||||
newStatus.notifyObservers(&powerStatus2);
|
||||
|
||||
// Mirror battery level to the BLE Battery Service (0x2A19); the platform layer clamps and dedupes.
|
||||
if (hasBattery == OptTrue)
|
||||
updateBatteryLevel(powerStatus2.getBatteryChargePercent());
|
||||
#ifdef DEBUG_HEAP
|
||||
if (lastheap != memGet.getFreeHeap()) {
|
||||
// Use stack-allocated buffer to avoid heap allocations in monitoring code
|
||||
|
||||
@@ -158,8 +158,8 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
|
||||
ignoreRequest = true;
|
||||
return NULL;
|
||||
} else {
|
||||
ignoreRequest = false; // Don't ignore requests anymore
|
||||
meshtastic_User &u = owner;
|
||||
ignoreRequest = false; // Don't ignore requests anymore
|
||||
meshtastic_User u = owner; // deliberate copy: the licensed strip below must not clobber the global owner state
|
||||
|
||||
// Strip the public key if the user is licensed
|
||||
if (u.is_licensed && u.public_key.size > 0) {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
// meshtastic_ServiceEnvelope that automatically releases dynamically allocated memory when it goes out of scope.
|
||||
struct DecodedServiceEnvelope : public meshtastic_ServiceEnvelope {
|
||||
DecodedServiceEnvelope(const uint8_t *payload, size_t length);
|
||||
DecodedServiceEnvelope(DecodedServiceEnvelope &) = delete;
|
||||
// const-qualified so std::variant instantiation works on Apple libc++ (copying stays ill-formed either way)
|
||||
DecodedServiceEnvelope(const DecodedServiceEnvelope &) = delete;
|
||||
DecodedServiceEnvelope(DecodedServiceEnvelope &&);
|
||||
~DecodedServiceEnvelope();
|
||||
// Clients must check that this is true before using.
|
||||
|
||||
@@ -40,6 +40,7 @@ constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
|
||||
|
||||
BLECharacteristic *fromNumCharacteristic;
|
||||
BLECharacteristic *BatteryCharacteristic;
|
||||
static int lastBatteryLevel = -1; // last value written to 0x2A19, to skip redundant writes/notifies
|
||||
BLECharacteristic *logRadioCharacteristic;
|
||||
BLEServer *bleServer;
|
||||
|
||||
@@ -718,6 +719,8 @@ void NimbleBluetooth::deinit()
|
||||
#endif
|
||||
|
||||
BLEDevice::deinit(true);
|
||||
BatteryCharacteristic = nullptr; // freed by deinit; clear so updateBatteryLevel() won't touch it
|
||||
lastBatteryLevel = -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -856,16 +859,31 @@ void NimbleBluetooth::setupService()
|
||||
BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
|
||||
(uint16_t)0x2a19, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
|
||||
BatteryCharacteristic->addDescriptor(batteryLevelDescriptor);
|
||||
// Seed an initial 0-100 level so an early read of 0x2A19 returns a valid value.
|
||||
uint8_t initialLevel = (powerStatus && powerStatus->getHasBattery()) ? powerStatus->getBatteryChargePercent() : 0;
|
||||
if (initialLevel > 100)
|
||||
initialLevel = 100;
|
||||
BatteryCharacteristic->setValue(&initialLevel, 1);
|
||||
lastBatteryLevel = initialLevel;
|
||||
batteryService->start();
|
||||
}
|
||||
|
||||
/// Given a level between 0-100, update the BLE attribute
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
if ((config.bluetooth.enabled == true) && nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
BatteryCharacteristic->setValue(&level, 1);
|
||||
if (!config.bluetooth.enabled || !BatteryCharacteristic)
|
||||
return;
|
||||
|
||||
if (level > 100) // 0x2A19 must stay within the BAS 0-100 range
|
||||
level = 100;
|
||||
if (level == lastBatteryLevel)
|
||||
return;
|
||||
lastBatteryLevel = level;
|
||||
|
||||
// Cache the value so a READ works without a subscriber; notify only when connected.
|
||||
BatteryCharacteristic->setValue(&level, 1);
|
||||
if (nimbleBluetooth && nimbleBluetooth->isConnected())
|
||||
BatteryCharacteristic->notify();
|
||||
}
|
||||
}
|
||||
|
||||
void NimbleBluetooth::clearBonds()
|
||||
|
||||
@@ -15,8 +15,9 @@ static BLECharacteristic fromRadio = BLECharacteristic(BLEUuid(FROMRADIO_UUID_16
|
||||
static BLECharacteristic toRadio = BLECharacteristic(BLEUuid(TORADIO_UUID_16));
|
||||
static BLECharacteristic logRadio = BLECharacteristic(BLEUuid(LOGRADIO_UUID_16));
|
||||
|
||||
static BLEDis bledis; // DIS (Device Information Service) helper class instance
|
||||
static BLEBas blebas; // BAS (Battery Service) helper class instance
|
||||
static BLEDis bledis; // DIS (Device Information Service) helper class instance
|
||||
static BLEBas blebas; // BAS (Battery Service) helper class instance
|
||||
static int lastBatteryLevel = -1; // last value written to BAS, to skip redundant writes/notifies
|
||||
#ifndef BLE_DFU_SECURE
|
||||
static BLEDfu bledfu; // DFU software update helper service
|
||||
#else
|
||||
@@ -336,6 +337,7 @@ void NRF52Bluetooth::setup()
|
||||
LOG_INFO("Init the Battery Service");
|
||||
blebas.begin();
|
||||
blebas.write(0); // Unknown battery level for now
|
||||
lastBatteryLevel = 0;
|
||||
// Setup the Heart Rate Monitor service using
|
||||
// BLEService and BLECharacteristic classes
|
||||
LOG_INFO("Init the Mesh bluetooth service");
|
||||
@@ -355,6 +357,14 @@ void NRF52Bluetooth::resumeAdvertising()
|
||||
/// Given a level between 0-100, update the BLE attribute
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
if (!nrf52Bluetooth) // skip until the Battery Service has been begun in setup()
|
||||
return;
|
||||
|
||||
if (level > 100) // BAS battery level must stay within 0-100
|
||||
level = 100;
|
||||
if (level == lastBatteryLevel)
|
||||
return;
|
||||
lastBatteryLevel = level;
|
||||
blebas.write(level);
|
||||
}
|
||||
void NRF52Bluetooth::clearBonds()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "TestUtil.h"
|
||||
#include <cstdlib>
|
||||
#include <unity.h>
|
||||
|
||||
static void test_placeholder()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "TestUtil.h"
|
||||
#include <cstdlib>
|
||||
#include <unity.h>
|
||||
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
|
||||
@@ -55,7 +55,6 @@ build_flags_common =
|
||||
-lyaml-cpp
|
||||
-ljsoncpp
|
||||
!pkg-config --cflags jsoncpp --silence-errors || :
|
||||
-li2c
|
||||
-luv
|
||||
-std=gnu17
|
||||
-std=gnu++17
|
||||
|
||||
Reference in New Issue
Block a user