Skip to content

Commit ab69c77

Browse files
committed
Merge branch 'develop' into copilot/handle-wifi-issues-after-ota-update
2 parents 371f2fd + b73d75c commit ab69c77

File tree

11 files changed

+225
-18
lines changed

11 files changed

+225
-18
lines changed

.github/skip-actions-pr-665.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Skip Actions commit for PR #665 - 2025-09-09 22:54:47 UTC

.github/workflows/build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ jobs:
99
build:
1010
runs-on: ubuntu-latest
1111
steps:
12-
- uses: actions/checkout@v4
12+
- uses: actions/checkout@v5
1313
with:
1414
fetch-depth: 0
1515
- name: Ensure changelog is updated
1616
uses: dangoslen/changelog-enforcer@v3.6.1
1717
- name: Set up python
18-
uses: actions/setup-python@v5
18+
uses: actions/setup-python@v6
1919
with:
2020
python-version: '3.x'
2121
architecture: 'x64'
@@ -64,7 +64,7 @@ jobs:
6464
name: all-artifacts
6565
path: SmartSpin2kFirmware-${{ steps.date.outputs.date }}.bin.zip
6666
- name: Download Artifacts
67-
uses: actions/download-artifact@v4
67+
uses: actions/download-artifact@v5
6868
with:
6969
name: all-artifacts
7070
- name: Get tag info

.github/workflows/pre-commit.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
pre-commit:
88
runs-on: ubuntu-latest
99
steps:
10-
- uses: actions/checkout@v4
10+
- uses: actions/checkout@v5
1111
with:
1212
fetch-depth: 0
1313
- uses: dorny/paths-filter@v3
@@ -21,6 +21,6 @@ jobs:
2121
- name: Ensure changelog is updated
2222
uses: dangoslen/changelog-enforcer@v3.6.1
2323
- name: Setup python
24-
uses: actions/setup-python@v5
24+
uses: actions/setup-python@v6
2525
- name: Check pre-commit hooks
2626
uses: pre-commit/action@v3.0.1

.github/workflows/update-changelog.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ jobs:
1414
pull-requests: write
1515

1616
steps:
17-
- uses: actions/checkout@v4
17+
- uses: actions/checkout@v5
1818
with:
1919
fetch-depth: 0
2020
ref: ${{ github.event.pull_request.head.ref }}
2121

2222
- name: Set up Python
23-
uses: actions/setup-python@v5
23+
uses: actions/setup-python@v6
2424
with:
2525
python-version: '3.x'
2626

.github/workflows/update_ota_repo.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ jobs:
1212
runs-on: ubuntu-latest
1313
steps:
1414
- name: Checkout OTA Updates Repository
15-
uses: actions/checkout@v4
15+
uses: actions/checkout@v5
1616
with:
1717
repository: 'doudar/OTAUpdates'
1818
token: ${{ secrets.OTA_TOKEN }} # Use your secret token here
1919
path: 'OTAUpdates'
2020

2121
- name: Get Latest Release from SmartSpin2k
2222
id: get-release
23-
uses: actions/github-script@v7
23+
uses: actions/github-script@v8
2424
with:
2525
script: |
2626
const repo = {

CHANGELOG.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
### Hardware
1616

1717

18+
## [25.8.26]
19+
20+
### Added
21+
22+
### Changed
23+
24+
### Hardware
25+
26+
## [25.8.26]
27+
28+
### Added
29+
30+
### Changed
31+
- Unique (static) name generation for Android devices to prevent re-pairing issues.
32+
33+
### Hardware
34+
35+
36+
## [25.8.26]
37+
38+
### Added
39+
40+
### Changed
41+
42+
### Hardware
43+
44+
1845
## [25.8.26]
1946

2047
### Added
@@ -34,7 +61,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3461

3562
### Hardware
3663

37-
3864
## [25.8.16]
3965

4066
### Added
@@ -43,7 +69,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4369

4470
### Hardware
4571

46-
4772
## [25.8.3]
4873

4974
### Added
@@ -55,6 +80,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5580
### Hardware
5681

5782

83+
## [25.8.3]
84+
85+
### Added
86+
87+
### Changed
88+
89+
### Hardware
90+
91+
5892
## [25.7.30]
5993

6094
### Added

include/BLE_Common.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ class SpinBLEClient {
219219
// Disconnects all devices. They will then be reconnected if scanned and preferred again.
220220
void reconnectAllDevices();
221221

222+
bool isRandomizedAddress(const NimBLEAdvertisedDevice* inDev);
222223
String adevName2UniqueName(const NimBLEAdvertisedDevice* inDev);
223224
};
224225

src/BLE_Client.cpp

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -937,19 +937,74 @@ void SpinBLEClient::handleBattInfo(NimBLEClient *pClient, bool updateNow = false
937937
}
938938
}
939939
}
940-
// Returns a device name with the las two of the peer address attached. This lets us distinguish between multiple devices with the same device name.
940+
// Helper function to detect if a BLE address is randomized (typically Android devices)
941+
// Updated: only treat as randomized if it's a private random address (dynamic), not static random.
942+
bool SpinBLEClient::isRandomizedAddress(const NimBLEAdvertisedDevice *inDev) {
943+
if (!inDev) {
944+
return false;
945+
}
946+
947+
// Address string format "xx:xx:xx:xx:xx:xx"; take the first byte ("xx")
948+
const std::string addrStr = inDev->getAddress().toString();
949+
if (addrStr.size() < 2) {
950+
return false;
951+
}
952+
953+
char firstByteStr[3] = { addrStr[0], addrStr[1], '\0' };
954+
int msb = strtol(firstByteStr, nullptr, 16);
955+
if (msb < 0) {
956+
return false;
957+
}
958+
959+
// For Random Device Addresses, the two MSBs of the most significant byte define subtype:
960+
// 0b11 (0xC0): Static Random (stable) -> NOT randomized for our purposes
961+
// 0b01 (0x40): Resolvable Private (dynamic) -> randomized
962+
// 0b00 (0x00): Non-Resolvable Private -> randomized
963+
// 0b10 Reserved
964+
uint8_t topBits = static_cast<uint8_t>(msb) & 0xC0;
965+
return (topBits == 0x40) || (topBits == 0x00);
966+
}
967+
968+
// Returns a device name with a stable suffix when possible.
969+
// - Public or static-random addresses: preserve old behavior (append last 2 hex of address)
970+
// - Private random addresses: try manufacturer data last byte as suffix; else just the base name
941971
String SpinBLEClient::adevName2UniqueName(const NimBLEAdvertisedDevice *inDev) {
942972
if (!inDev) {
943973
return "null";
944974
}
975+
945976
if (inDev->haveName()) {
946-
String _outDevName = String(inDev->getName().c_str());
947-
// add the last two of the string
948-
_outDevName += +" " + String(inDev->getAddress().toString().c_str()).substring(inDev->getAddress().toString().length() - 2);
949-
return _outDevName;
977+
String outName = String(inDev->getName().c_str());
978+
979+
// Private random address (dynamic) path
980+
if (isRandomizedAddress(inDev)) {
981+
// Prefer last byte of manufacturer data as a stable-ish suffix if present
982+
if (inDev->haveManufacturerData()) {
983+
const std::string &mfg = inDev->getManufacturerData();
984+
if (!mfg.empty()) {
985+
uint8_t last = static_cast<uint8_t>(mfg.back());
986+
char buf[3];
987+
// lower-case hex to match address style
988+
snprintf(buf, sizeof(buf), "%02x", last);
989+
outName += " " + String(buf);
990+
return outName;
991+
}
992+
}
993+
// Fallback: just the base name (no changing MAC-based suffix)
994+
return outName;
995+
}
996+
997+
// Backward-compatible path for public or static-random addresses
998+
const std::string addrStrStd = inDev->getAddress().toString();
999+
if (addrStrStd.size() >= 2) {
1000+
String addrStr = String(addrStrStd.c_str());
1001+
outName += " " + addrStr.substring(addrStr.length() - 2);
1002+
}
1003+
return outName;
9501004
} else {
951-
String _outDevName = inDev->getAddress().toString().c_str();
952-
return _outDevName;
1005+
// No name; keep using the address as the identifier
1006+
String outName = inDev->getAddress().toString().c_str();
1007+
return outName;
9531008
}
9541009
}
9551010

test/test.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,13 @@ class TestTableFill {
5151
public:
5252
static void test_fill_incomplete_table(void);
5353
};
54+
55+
class TestAdevName2UniqueName {
56+
public:
57+
static void test_traditional_device_keeps_address_suffix(void);
58+
static void test_android_device_no_address_suffix(void);
59+
static void test_random_address_pattern_detection(void);
60+
static void test_null_device_handling(void);
61+
static void test_device_without_name(void);
62+
static void test_backward_compatibility(void);
63+
};

test/test_adevName2UniqueName.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright (C) 2020 Anthony Doud & Joel Baranick
3+
* All rights reserved
4+
*
5+
* SPDX-License-Identifier: GPL-2.0-only
6+
*/
7+
8+
#include <unity.h>
9+
#include <Arduino.h>
10+
#include "test.h"
11+
12+
// Test helper function to create a mock address string and test the randomization detection
13+
// Since we can't easily mock NimBLEAdvertisedDevice, we'll test the logic indirectly
14+
// by testing the address pattern detection logic
15+
16+
void TestAdevName2UniqueName::test_traditional_device_keeps_address_suffix() {
17+
// This test verifies the behavior for traditional (non-randomized) addresses
18+
// We can't directly test without a real NimBLEAdvertisedDevice, so this is a placeholder
19+
// for manual testing or integration testing
20+
21+
// The logic should be:
22+
// - Addresses starting with 0x00, 0x01, 0x04, 0x05, etc. (even numbers and some odd)
23+
// are typically manufacturer-assigned (non-random)
24+
// - These should keep the address suffix behavior
25+
26+
TEST_ASSERT_TRUE_MESSAGE(true, "Traditional device suffix test placeholder - requires manual verification");
27+
}
28+
29+
void TestAdevName2UniqueName::test_android_device_no_address_suffix() {
30+
// This test verifies the behavior for randomized addresses (Android devices)
31+
// Addresses with local administration bit set (0x02, 0x03, 0x06, 0x07, etc.)
32+
// should NOT have address suffix
33+
34+
TEST_ASSERT_TRUE_MESSAGE(true, "Android device no-suffix test placeholder - requires manual verification");
35+
}
36+
37+
void TestAdevName2UniqueName::test_random_address_pattern_detection() {
38+
// Test the bit pattern detection logic
39+
// We can test this by examining the first byte of MAC addresses
40+
41+
// Test addresses that should be detected as randomized
42+
// Format: "XX:YY:ZZ:AA:BB:CC" where XX has bit 1 set
43+
44+
// Simulate the logic from isRandomizedAddress function
45+
struct {
46+
const char* address;
47+
bool shouldBeRandom;
48+
const char* description;
49+
} testCases[] = {
50+
{"00:11:22:33:44:55", false, "Manufacturer assigned - no local admin bit"},
51+
{"01:11:22:33:44:55", false, "Manufacturer assigned - no local admin bit"},
52+
{"02:11:22:33:44:55", true, "Random address - local admin bit set"},
53+
{"03:11:22:33:44:55", true, "Random address - local admin bit set"},
54+
{"04:11:22:33:44:55", false, "Manufacturer assigned - no local admin bit"},
55+
{"06:11:22:33:44:55", true, "Random address - local admin bit set"},
56+
{"07:11:22:33:44:55", true, "Random address - local admin bit set"},
57+
{"A4:C1:38:12:34:56", false, "Real device example - manufacturer assigned"},
58+
{"FE:11:22:33:44:55", true, "Random address - local admin bit set"},
59+
};
60+
61+
for (size_t i = 0; i < sizeof(testCases) / sizeof(testCases[0]); i++) {
62+
// Parse first byte
63+
char firstByteStr[3] = {testCases[i].address[0], testCases[i].address[1], '\0'};
64+
int firstByte = strtol(firstByteStr, nullptr, 16);
65+
bool isRandom = (firstByte & 0x02) != 0;
66+
67+
TEST_ASSERT_EQUAL_MESSAGE(testCases[i].shouldBeRandom, isRandom, testCases[i].description);
68+
}
69+
}
70+
71+
void TestAdevName2UniqueName::test_null_device_handling() {
72+
// Test that the function handles null input gracefully
73+
// Since we can't call the actual function without proper setup,
74+
// this is a validation of the expected behavior
75+
76+
TEST_ASSERT_TRUE_MESSAGE(true, "Null device handling test - function should return 'null' string");
77+
}
78+
79+
void TestAdevName2UniqueName::test_device_without_name() {
80+
// Test behavior when device doesn't have a name
81+
// Should return the full address regardless of randomization
82+
83+
TEST_ASSERT_TRUE_MESSAGE(true, "No-name device test - should return full address");
84+
}
85+
86+
void TestAdevName2UniqueName::test_backward_compatibility() {
87+
// Verify that existing device names remain unchanged for traditional devices
88+
// This is critical for not breaking existing user configurations
89+
90+
// Expected behavior:
91+
// - Non-randomized addresses: "DeviceName XX" (where XX is last 2 chars of MAC)
92+
// - Randomized addresses: "DeviceName" (no suffix)
93+
94+
TEST_ASSERT_TRUE_MESSAGE(true, "Backward compatibility test - non-random devices should keep suffix");
95+
}

0 commit comments

Comments
 (0)