Skip to content

Commit 7fcaa55

Browse files
committed
Move formatAddress to FormatUtil, add tests and javadoc example
- FormatUtil.formatMacAddress(long) and formatMacAddress(String): shared MAC address formatting used by Windows and macOS implementations - FormatUtilTest: unit tests for both overloads - BluetoothDeviceTest: shared integration test for all platforms - BluetoothDevice interface: add usage code example in javadoc - Remove duplicated formatAddress from all platform implementations - Add VersionHelpers.isVistaOrGreater gate on Windows implementations - Fix library name to BluetoothApis (from bthprops.cpl)
1 parent 0aa2a61 commit 7fcaa55

10 files changed

Lines changed: 159 additions & 46 deletions

File tree

oshi-common/src/main/java/oshi/hardware/BluetoothDevice.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@
1313
* <p>
1414
* Bluetooth devices are enumerated per adapter. Each device reports its name, MAC address, major device class,
1515
* connection/pairing state, and battery level when available.
16+
* <p>
17+
* Example usage:
18+
*
19+
* <pre>{@code
20+
* for (BluetoothDevice device : hal.getBluetoothDevices()) {
21+
* System.out.printf("%s [%s] %s%s (battery: %s)%n", device.getName(), device.getAddress(),
22+
* device.getMajorDeviceClass(), device.isConnected() ? " *connected*" : "",
23+
* device.getBatteryLevel() >= 0 ? device.getBatteryLevel() + "%" : "N/A");
24+
* }
25+
* }</pre>
1626
*/
1727
@PublicApi
1828
@Immutable

oshi-common/src/main/java/oshi/hardware/common/platform/linux/LinuxBluetoothDevice.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.List;
1111
import java.util.Locale;
1212
import java.util.Map;
13+
import java.util.regex.Pattern;
1314

1415
import oshi.annotation.concurrent.Immutable;
1516
import oshi.hardware.BluetoothDevice;
@@ -29,6 +30,7 @@ public final class LinuxBluetoothDevice extends AbstractBluetoothDevice {
2930

3031
private static final String SYS_BLUETOOTH = SysPath.SYS + "class/bluetooth/";
3132
private static final String VAR_LIB_BLUETOOTH = "/var/lib/bluetooth/";
33+
private static final Pattern MAC_PATTERN = Pattern.compile("(?i)([0-9a-f]{2}:){5}[0-9a-f]{2}");
3234

3335
private LinuxBluetoothDevice(String name, String address, String majorDeviceClass, boolean connected,
3436
boolean paired, int batteryLevel, String adapterName) {
@@ -71,7 +73,7 @@ static List<BluetoothDevice> queryBluetoothDevices(String sysBluetoothPath, Stri
7173
File adapterStateDir = new File(varLibPath + adapterAddress.toUpperCase(Locale.ROOT));
7274
if (!adapterStateDir.isDirectory()) {
7375
// Try lowercase (some systems)
74-
adapterStateDir = new File(varLibPath + adapterAddress);
76+
adapterStateDir = new File(varLibPath + adapterAddress.toLowerCase(Locale.ROOT));
7577
}
7678
if (!adapterStateDir.isDirectory()) {
7779
continue;
@@ -85,7 +87,7 @@ static List<BluetoothDevice> queryBluetoothDevices(String sysBluetoothPath, Stri
8587
for (File deviceDir : deviceDirs) {
8688
String dirName = deviceDir.getName();
8789
// Device directories are MAC addresses (XX:XX:XX:XX:XX:XX)
88-
if (!dirName.contains(":") || dirName.length() != 17) {
90+
if (!MAC_PATTERN.matcher(dirName).matches()) {
8991
continue;
9092
}
9193
File infoFile = new File(deviceDir, "info");

oshi-common/src/main/java/oshi/util/FormatUtil.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,43 @@ public static String formatError(int errorCode) {
221221
public static int roundToInt(double x) {
222222
return (int) Math.round(x);
223223
}
224+
225+
/**
226+
* Formats a 6-byte MAC address stored in the lower 48 bits of a long to colon-separated uppercase hex (e.g.,
227+
* {@code AA:BB:CC:DD:EE:FF}).
228+
*
229+
* @param addr the MAC address as a long (lower 48 bits)
230+
* @return the formatted MAC address string
231+
*/
232+
public static String formatMacAddress(long addr) {
233+
return String.format(Locale.ROOT, "%02X:%02X:%02X:%02X:%02X:%02X", (addr >> 40) & 0xFF, (addr >> 32) & 0xFF,
234+
(addr >> 24) & 0xFF, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF);
235+
}
236+
237+
/**
238+
* Normalizes a MAC address string (which may use dashes, colons, or no separators) to colon-separated uppercase hex
239+
* (e.g., {@code AA:BB:CC:DD:EE:FF}).
240+
*
241+
* @param raw the raw MAC address string
242+
* @return the normalized MAC address, or the original string uppercased if it cannot be parsed as 6 bytes
243+
*/
244+
public static String formatMacAddress(String raw) {
245+
String cleaned = raw.replace("-", "").replace(":", "").trim();
246+
if (cleaned.length() == 12 && isHex(cleaned)) {
247+
return String.format(Locale.ROOT, "%s:%s:%s:%s:%s:%s", cleaned.substring(0, 2), cleaned.substring(2, 4),
248+
cleaned.substring(4, 6), cleaned.substring(6, 8), cleaned.substring(8, 10),
249+
cleaned.substring(10, 12)).toUpperCase(Locale.ROOT);
250+
}
251+
return raw.toUpperCase(Locale.ROOT);
252+
}
253+
254+
private static boolean isHex(String s) {
255+
for (int i = 0; i < s.length(); i++) {
256+
char c = s.charAt(i);
257+
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
258+
return false;
259+
}
260+
}
261+
return true;
262+
}
224263
}

oshi-common/src/test/java/oshi/util/FormatUtilTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,19 @@ void testRoundToInt() {
157157
assertThat("Improper rounding 1", FormatUtil.roundToInt(1d), is(1));
158158
}
159159

160+
@Test
161+
void testFormatMacAddressFromLong() {
162+
assertThat(FormatUtil.formatMacAddress(0xAABBCCDDEEFFL), is("AA:BB:CC:DD:EE:FF"));
163+
assertThat(FormatUtil.formatMacAddress(0x000000000000L), is("00:00:00:00:00:00"));
164+
assertThat(FormatUtil.formatMacAddress(0x112233445566L), is("11:22:33:44:55:66"));
165+
}
166+
167+
@Test
168+
void testFormatMacAddressFromString() {
169+
assertThat(FormatUtil.formatMacAddress("aa-bb-cc-dd-ee-ff"), is("AA:BB:CC:DD:EE:FF"));
170+
assertThat(FormatUtil.formatMacAddress("11:22:33:44:55:66"), is("11:22:33:44:55:66"));
171+
assertThat(FormatUtil.formatMacAddress("aabbccddeeff"), is("AA:BB:CC:DD:EE:FF"));
172+
assertThat(FormatUtil.formatMacAddress("abcd"), is("ABCD"));
173+
}
174+
160175
}

oshi-core-ffm/src/main/java/oshi/hardware/platform/mac/MacBluetoothDeviceFFM.java

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
import java.util.ArrayList;
88
import java.util.Collections;
99
import java.util.List;
10-
import java.util.Locale;
1110

1211
import oshi.annotation.concurrent.Immutable;
1312
import oshi.ffm.mac.IOKit.IOIterator;
1413
import oshi.ffm.mac.IOKit.IORegistryEntry;
1514
import oshi.ffm.util.platform.mac.IOKitUtilFFM;
1615
import oshi.hardware.BluetoothDevice;
1716
import oshi.hardware.common.AbstractBluetoothDevice;
17+
import oshi.util.FormatUtil;
1818

1919
/**
2020
* macOS Bluetooth device enumeration via FFM/IOKit (IOBluetoothDevice).
@@ -50,7 +50,7 @@ public static List<BluetoothDevice> getBluetoothDevices() {
5050
if (address == null) {
5151
address = "";
5252
} else {
53-
address = formatAddress(address);
53+
address = FormatUtil.formatMacAddress(address);
5454
}
5555
Long cod = device.getLongProperty("ClassOfDevice");
5656
String majorClass = parseMajorDeviceClass(cod != null ? cod.intValue() : 0);
@@ -69,13 +69,4 @@ public static List<BluetoothDevice> getBluetoothDevices() {
6969
return Collections.unmodifiableList(devices);
7070
}
7171

72-
private static String formatAddress(String raw) {
73-
String cleaned = raw.replace("-", "").replace(":", "").trim();
74-
if (cleaned.length() == 12) {
75-
return String.format(Locale.ROOT, "%s:%s:%s:%s:%s:%s", cleaned.substring(0, 2), cleaned.substring(2, 4),
76-
cleaned.substring(4, 6), cleaned.substring(6, 8), cleaned.substring(8, 10),
77-
cleaned.substring(10, 12)).toUpperCase(Locale.ROOT);
78-
}
79-
return raw.toUpperCase(Locale.ROOT);
80-
}
8172
}

oshi-core-ffm/src/main/java/oshi/hardware/platform/windows/WindowsBluetoothDeviceFFM.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@
2020
import java.util.ArrayList;
2121
import java.util.Collections;
2222
import java.util.List;
23-
import java.util.Locale;
2423

2524
import org.slf4j.Logger;
2625
import org.slf4j.LoggerFactory;
2726

2827
import oshi.annotation.concurrent.Immutable;
2928
import oshi.ffm.windows.BluetoothApisFFM;
3029
import oshi.ffm.windows.Kernel32FFM;
30+
import oshi.ffm.windows.VersionHelpersFFM;
3131
import oshi.ffm.windows.WindowsForeignFunctions;
3232
import oshi.hardware.BluetoothDevice;
3333
import oshi.hardware.common.AbstractBluetoothDevice;
34+
import oshi.util.FormatUtil;
3435

3536
/**
3637
* Windows Bluetooth device enumeration via FFM (bthprops.cpl).
@@ -40,6 +41,8 @@ public final class WindowsBluetoothDeviceFFM extends AbstractBluetoothDevice {
4041

4142
private static final Logger LOG = LoggerFactory.getLogger(WindowsBluetoothDeviceFFM.class);
4243

44+
private static final boolean IS_VISTA_OR_GREATER = VersionHelpersFFM.IsWindowsVistaOrGreater();
45+
4346
private WindowsBluetoothDeviceFFM(String name, String address, String majorDeviceClass, boolean connected,
4447
boolean paired, int batteryLevel, String adapterName) {
4548
super(name, address, majorDeviceClass, connected, paired, batteryLevel, adapterName);
@@ -51,6 +54,9 @@ private WindowsBluetoothDeviceFFM(String name, String address, String majorDevic
5154
* @return a list of {@link BluetoothDevice} objects
5255
*/
5356
public static List<BluetoothDevice> getBluetoothDevices() {
57+
if (!IS_VISTA_OR_GREATER) {
58+
return Collections.emptyList();
59+
}
5460
List<BluetoothDevice> devices = new ArrayList<>();
5561
try (Arena arena = Arena.ofConfined()) {
5662
MemorySegment radioParams = arena.allocate(BLUETOOTH_FIND_RADIO_PARAMS_LAYOUT);
@@ -134,7 +140,7 @@ private static WindowsBluetoothDeviceFFM parseDeviceInfo(MemorySegment info, Str
134140
String name = readDeviceName(info,
135141
BLUETOOTH_DEVICE_INFO_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("szName")));
136142
String majorClass = AbstractBluetoothDevice.parseMajorDeviceClass(cod);
137-
String address = formatAddress(addr);
143+
String address = FormatUtil.formatMacAddress(addr);
138144
return new WindowsBluetoothDeviceFFM(name, address, majorClass, connected, paired, -1, adapterName);
139145
}
140146

@@ -149,9 +155,4 @@ private static String readDeviceName(MemorySegment seg, long offset) {
149155
}
150156
return sb.toString();
151157
}
152-
153-
private static String formatAddress(long addr) {
154-
return String.format(Locale.ROOT, "%02X:%02X:%02X:%02X:%02X:%02X", (addr >> 40) & 0xFF, (addr >> 32) & 0xFF,
155-
(addr >> 24) & 0xFF, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF);
156-
}
157158
}

oshi-core/src/main/java/oshi/hardware/platform/mac/MacBluetoothDeviceJNA.java

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import java.util.ArrayList;
88
import java.util.Collections;
99
import java.util.List;
10-
import java.util.Locale;
1110

1211
import com.sun.jna.platform.mac.IOKit.IOIterator;
1312
import com.sun.jna.platform.mac.IOKit.IORegistryEntry;
@@ -16,6 +15,7 @@
1615
import oshi.annotation.concurrent.Immutable;
1716
import oshi.hardware.BluetoothDevice;
1817
import oshi.hardware.common.AbstractBluetoothDevice;
18+
import oshi.util.FormatUtil;
1919

2020
/**
2121
* macOS Bluetooth device enumeration via IOKit (IOBluetoothDevice).
@@ -51,7 +51,7 @@ public static List<BluetoothDevice> getBluetoothDevices() {
5151
if (address == null) {
5252
address = "";
5353
} else {
54-
address = formatAddress(address);
54+
address = FormatUtil.formatMacAddress(address);
5555
}
5656
Long cod = device.getLongProperty("ClassOfDevice");
5757
String majorClass = parseMajorDeviceClass(cod != null ? cod.intValue() : 0);
@@ -69,20 +69,4 @@ public static List<BluetoothDevice> getBluetoothDevices() {
6969
iter.release();
7070
return Collections.unmodifiableList(devices);
7171
}
72-
73-
/**
74-
* Formats a macOS Bluetooth address (e.g., "aa-bb-cc-dd-ee-ff" or "aabbccddeeff") to colon-separated uppercase.
75-
*
76-
* @param raw the raw address string
77-
* @return formatted address
78-
*/
79-
private static String formatAddress(String raw) {
80-
String cleaned = raw.replace("-", "").replace(":", "").trim();
81-
if (cleaned.length() == 12) {
82-
return String.format(Locale.ROOT, "%s:%s:%s:%s:%s:%s", cleaned.substring(0, 2), cleaned.substring(2, 4),
83-
cleaned.substring(4, 6), cleaned.substring(6, 8), cleaned.substring(8, 10),
84-
cleaned.substring(10, 12)).toUpperCase(Locale.ROOT);
85-
}
86-
return raw.toUpperCase(Locale.ROOT);
87-
}
8872
}

oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsBluetoothDeviceJNA.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import java.util.ArrayList;
88
import java.util.Collections;
99
import java.util.List;
10-
import java.util.Locale;
1110

1211
import com.sun.jna.platform.win32.Kernel32;
12+
import com.sun.jna.platform.win32.VersionHelpers;
1313
import com.sun.jna.platform.win32.WinNT.HANDLE;
1414
import com.sun.jna.ptr.PointerByReference;
1515

@@ -21,13 +21,16 @@
2121
import oshi.jna.platform.windows.BluetoothApis.BLUETOOTH_DEVICE_SEARCH_PARAMS;
2222
import oshi.jna.platform.windows.BluetoothApis.BLUETOOTH_FIND_RADIO_PARAMS;
2323
import oshi.jna.platform.windows.BluetoothApis.BLUETOOTH_RADIO_INFO;
24+
import oshi.util.FormatUtil;
2425

2526
/**
2627
* Windows Bluetooth device enumeration via the Bluetooth API (bthprops.cpl).
2728
*/
2829
@Immutable
2930
public final class WindowsBluetoothDeviceJNA extends AbstractBluetoothDevice {
3031

32+
private static final boolean IS_VISTA_OR_GREATER = VersionHelpers.IsWindowsVistaOrGreater();
33+
3134
private WindowsBluetoothDeviceJNA(String name, String address, String majorDeviceClass, boolean connected,
3235
boolean paired, int batteryLevel, String adapterName) {
3336
super(name, address, majorDeviceClass, connected, paired, batteryLevel, adapterName);
@@ -39,6 +42,9 @@ private WindowsBluetoothDeviceJNA(String name, String address, String majorDevic
3942
* @return a list of {@link BluetoothDevice} objects
4043
*/
4144
public static List<BluetoothDevice> getBluetoothDevices() {
45+
if (!IS_VISTA_OR_GREATER) {
46+
return Collections.emptyList();
47+
}
4248
List<BluetoothDevice> devices = new ArrayList<>();
4349
BLUETOOTH_FIND_RADIO_PARAMS radioParams = new BLUETOOTH_FIND_RADIO_PARAMS();
4450
PointerByReference phRadio = new PointerByReference();
@@ -98,15 +104,11 @@ private static void queryDevicesForRadio(HANDLE hRadio, String adapterName, List
98104

99105
private static WindowsBluetoothDeviceJNA parseDeviceInfo(BLUETOOTH_DEVICE_INFO info, String adapterName) {
100106
String name = new String(info.szName).trim();
101-
String address = formatAddress(info.Address);
107+
String address = FormatUtil.formatMacAddress(info.Address.getAddress());
102108
String majorClass = AbstractBluetoothDevice.parseMajorDeviceClass(info.ulClassofDevice);
103109
boolean connected = info.fConnected;
104110
boolean paired = info.fRemembered;
105111
return new WindowsBluetoothDeviceJNA(name, address, majorClass, connected, paired, -1, adapterName);
106112
}
107113

108-
private static String formatAddress(long addr) {
109-
return String.format(Locale.ROOT, "%02X:%02X:%02X:%02X:%02X:%02X", (addr >> 40) & 0xFF, (addr >> 32) & 0xFF,
110-
(addr >> 24) & 0xFF, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF);
111-
}
112114
}

oshi-core/src/main/java/oshi/jna/platform/windows/BluetoothApis.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.sun.jna.Native;
88
import com.sun.jna.Structure;
99
import com.sun.jna.Structure.FieldOrder;
10+
import com.sun.jna.Union;
1011
import com.sun.jna.platform.win32.WinNT.HANDLE;
1112
import com.sun.jna.ptr.PointerByReference;
1213
import com.sun.jna.win32.W32APIOptions;
@@ -23,14 +24,44 @@ public interface BluetoothApis extends com.sun.jna.win32.StdCallLibrary {
2324
/** Maximum Bluetooth device name length. */
2425
int BLUETOOTH_MAX_NAME_SIZE = 248;
2526

27+
/**
28+
* BLUETOOTH_ADDRESS union. Contains a Bluetooth device address as either a ULONGLONG or a 6-byte array.
29+
*/
30+
class BLUETOOTH_ADDRESS extends Union {
31+
public long ullLong;
32+
public byte[] rgBytes = new byte[6];
33+
34+
/**
35+
* Gets the address as a long.
36+
*
37+
* @return the address
38+
*/
39+
public long getAddress() {
40+
setType("ullLong");
41+
read();
42+
return ullLong;
43+
}
44+
45+
/**
46+
* Gets the address as a 6-byte array.
47+
*
48+
* @return the address bytes
49+
*/
50+
public byte[] getBytes() {
51+
setType("rgBytes");
52+
read();
53+
return rgBytes;
54+
}
55+
}
56+
2657
/**
2758
* BLUETOOTH_DEVICE_INFO structure.
2859
*/
2960
@FieldOrder({ "dwSize", "Address", "ulClassofDevice", "fConnected", "fRemembered", "fAuthenticated", "stLastSeen",
3061
"stLastUsed", "szName" })
3162
class BLUETOOTH_DEVICE_INFO extends Structure {
3263
public int dwSize;
33-
public long Address;
64+
public BLUETOOTH_ADDRESS Address;
3465
public int ulClassofDevice;
3566
public boolean fConnected;
3667
public boolean fRemembered;
@@ -97,7 +128,7 @@ public BLUETOOTH_FIND_RADIO_PARAMS() {
97128
@FieldOrder({ "dwSize", "address", "szName", "ulClassofDevice", "lmpSubversion", "manufacturer" })
98129
class BLUETOOTH_RADIO_INFO extends Structure {
99130
public int dwSize;
100-
public long address;
131+
public BLUETOOTH_ADDRESS address;
101132
public char[] szName = new char[BLUETOOTH_MAX_NAME_SIZE];
102133
public int ulClassofDevice;
103134
public short lmpSubversion;

0 commit comments

Comments
 (0)