Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Normalize line endings to LF
* text=auto eol=lf
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ with other Android tablets, phones, and TVs.
- Curated application catalog with ARM64 and ARMv7 variants
- Local APK installation
- Screen mirroring, screenshots, and screen recording
- Device file manager for browsing, uploading, and downloading files
- Installed-app, launcher, rotation, and developer-setting management
- Windows, macOS, and Linux desktop builds

Expand Down Expand Up @@ -48,6 +49,34 @@ normally require a pairing port and six-digit pairing code before connecting.
The desktop builds support `adb pair`; the experimental mobile ADB client does
not currently support pairing a new device.

### Automatic WiFi connection

Desktop builds always maintain wireless ADB connections automatically. After
you connect an Android device wirelessly once, OpenPelo verifies its manufacturer
and hardware serial, enables legacy ADB on port 5555, and remembers its address.
Setup may briefly interrupt the original connection. This works with any
manufacturer whose firmware supports legacy TCP/IP ADB and reports a usable
hardware serial. Devices connected only by USB are not switched automatically.

While OpenPelo is running, it retries remembered devices when disconnected,
including after reopening the app. This can reconnect even when the tablet's
**Wireless debugging** toggle is off. Retry delays increase from 10 seconds to
five minutes when a device is unavailable. When both wireless connections are
present, OpenPelo shows the port-5555 connection. Peloton devices appear at the
top of the device list; all other devices remain available, and your selected
target is preserved. It verifies the manufacturer and hardware serial
before using a remembered address, so a reassigned IP cannot silently select a
different device.

Legacy ADB is **unencrypted**: use this feature only on a trusted local network.
Rebooting the tablet may
remove the listener. If that happens, or its IP changes while disconnected,
connect normally again so OpenPelo can set it up and remember the new address.

Tested on a Peloton PLTN-RB1VO-2 running Android 11: port 5555 accepted a fresh
connection with Wireless debugging off. Survival of the reported approximately
24-hour automatic shutdown has not yet been verified.

## Build from source

### Requirements
Expand Down
32 changes: 32 additions & 0 deletions lib/models/device_file_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class DeviceFileEntry {
final String name;
final String path;
final bool isDirectory;
final bool isSymlink;
final int? sizeBytes;
final String? modified;

const DeviceFileEntry({
required this.name,
required this.path,
required this.isDirectory,
this.isSymlink = false,
this.sizeBytes,
this.modified,
});
}

String formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
const units = ['KB', 'MB', 'GB', 'TB'];
double value = bytes / 1024;
var unit = 0;
while (value >= 1024 && unit < units.length - 1) {
value /= 1024;
unit++;
}
final text = value >= 100
? value.toStringAsFixed(0)
: value.toStringAsFixed(1);
return '$text ${units[unit]}';
}
33 changes: 30 additions & 3 deletions lib/models/device_model.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:convert';

class DeviceModel {
final String serial;
final String status;
Expand All @@ -6,6 +8,8 @@ class DeviceModel {
final String? port;
final String? name; // manufacturer + model
final String? abi; // arm64-v8a or armeabi-v7a
final String? hardwareSerial;
final String? manufacturer;

DeviceModel({
required this.serial,
Expand All @@ -15,12 +19,24 @@ class DeviceModel {
this.port,
this.name,
this.abi,
this.hardwareSerial,
this.manufacturer,
});

String? get identityKey => manufacturer == null || hardwareSerial == null
? null
: jsonEncode([manufacturer, hardwareSerial]);

bool get isPeloton => RegExp(
r'^peloton(?:\s|$)',
caseSensitive: false,
).hasMatch(manufacturer ?? name ?? '');

String get displayName {
if (transport == 'wifi' && ip != null) {
return "$name • WiFi ($ip${port != null ? ':$port' : ''})";
}
if (transport == 'wifi') return "$name • WiFi";
return "$name • USB";
}

Expand All @@ -34,10 +50,21 @@ class DeviceModel {
ip == other.ip &&
port == other.port &&
name == other.name &&
abi == other.abi;
abi == other.abi &&
hardwareSerial == other.hardwareSerial &&
manufacturer == other.manufacturer;
}

@override
int get hashCode =>
Object.hash(serial, status, transport, ip, port, name, abi);
int get hashCode => Object.hash(
serial,
status,
transport,
ip,
port,
name,
abi,
hardwareSerial,
manufacturer,
);
}
Loading
Loading