Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .github/workflows/build-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
push:
pull_request:

concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/publish-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,31 @@ jobs:
export APP_STORE_CONNECT_PRIVATE_KEY=`cat AuthKey.p8`
app-store-connect builds submit-to-testflight $BUILD_ID || true
app-store-connect beta-groups add-build $BUILD_ID --beta-group="Open beta testing" || true

deploy-web:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./chameleonultragui
steps:
- uses: actions/checkout@v3

- uses: subosito/flutter-action@v2
with:
channel: "beta"

- uses: dtolnay/rust-toolchain@nightly
with:
targets: wasm32-unknown-unknown
components: rust-src

- run: cargo install wasm-pack

- run: flutter pub get
- run: flutter build web --wasm --build-number ${{ github.run_number }}

- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy chameleonultragui/build/web --project-name=chameleonultragui --branch=main
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ Download it from Google Play Store: [Chameleon Ultra GUI](https://play.google.co

Or, plain [APK](https://github.com/GameTec-live/ChameleonUltraGUI/releases/download/dev/apk.zip) (not signed, incompatible with Google Play version)

#### Web

You can access web version at [Chameleon Ultra GUI](https://web.chameleon.run). Requires Web Serial API to work

#### Pending stores:
- F-Store: not yet
- Chocolatey (Windows): not yet
Expand All @@ -56,6 +60,23 @@ Key:
You might need to add your user to the `dialout` or, on Arch Linux, to the `uucp` group for the app to talk to the device. If your user is not in this group, you may get serial or permission errors.
It is also highly recommended to either uninstall or disable ModemManager (`sudo systemctl disable --now modemmanager`) as many distros ship ModemManager and it may interfere with communication.

#### Note for Web build:
For key recovery to work you must set those headers (and use HTTPS):

```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```

Also you need a working CORS proxy to download firmware, for easier local development you might use `wrangler`:

```
mkcert -install
mkcert 127.0.0.1 localhost ::1
wrangler pages dev chameleonultragui/build/web --port 8788 --ip 127.0.0.1 --local-protocol https --https-cert-path ./127.0.0.1+2.pem --https-key-path ./127.0.0.1+2-key.pem
```


## Buy a Chameleon Ultra
- [Sneak Tech](https://sneaktechnology.com/product/chameleon-ultra/)
- [KSEC](https://labs.ksec.co.uk/product/proxgrind-chameleon-ultra/)
Expand Down
91 changes: 91 additions & 0 deletions chameleonultragui/hook/build.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'dart:io';
import 'package:hooks/hooks.dart';

String? _findWasmPack() {
final name = Platform.isWindows ? 'wasm-pack.exe' : 'wasm-pack';
final home =
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'] ?? '';

final candidates = [
'$home/.cargo/bin/$name',
...?Platform.environment['PATH']
?.split(Platform.isWindows ? ';' : ':')
.map((d) => '$d/$name'),
];

for (final path in candidates) {
if (File(path).existsSync()) return path;
}
return null;
}

void main(List<String> args) {
build(args, (input, output) async {
final packageRoot = input.packageRoot.toFilePath();
final rustDir = '${packageRoot}rust';
final webPkgDir = '${packageRoot}web/pkg';
final jsFile = '$webPkgDir/recovery_wasm.js';

if (!Directory(rustDir).existsSync()) return;

final isNativeBuild = input.config.buildAssetTypes.isNotEmpty;
if (isNativeBuild) return;

final exe = _findWasmPack();
if (exe == null) {
throw Exception(
'wasm-pack not found in PATH or ~/.cargo/bin.\n'
'Install it: cargo install wasm-pack\n'
'Or: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh',
);
}

print('[recovery-wasm] Building WASM with wasm-pack ($exe)...');
final result = await Process.run(
exe,
[
'build',
'--release',
'--target',
'no-modules',
'--no-opt',
'--out-dir',
webPkgDir,
],
workingDirectory: rustDir,
environment: Platform.environment,
);
stdout.write(result.stdout);
stderr.write(result.stderr);
if (result.exitCode != 0) {
throw Exception('wasm-pack failed with exit code ${result.exitCode}');
}

print('[recovery-wasm] Patching JS glue for shared-memory WASM...');
final file = File(jsFile);
if (file.existsSync()) {
var code = file.readAsStringSync();

// let -> var so wasm_bindgen lands on globalThis/window
code = code.replaceFirst('let wasm_bindgen', 'var wasm_bindgen');

// patch __wbg_init to handle {module_or_path, memory} from FRB workers
final pattern = RegExp(
r'async function __wbg_init\((\w+),\s*(\w+)\)\s*\{',
);
final match = pattern.firstMatch(code);
if (match != null && !code.contains('initSync(input.module_or_path')) {
final original = match.group(0)!;
final arg1 = match.group(1)!;
final patched =
'$original\n if (typeof $arg1 === \'object\' && $arg1 !== null && $arg1.module_or_path) {\n return initSync($arg1.module_or_path, $arg1.memory);\n }';
code = code.replaceFirst(original, patched);
}

file.writeAsStringSync(code);
print('[recovery-wasm] JS glue patched.');
}

print('[recovery-wasm] WASM build complete.');
});
}
4 changes: 2 additions & 2 deletions chameleonultragui/lib/bridge/chameleon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,8 @@ class ChameleonCommunicator {
return Darkside(
uid: bytesToU32(resp.data.sublist(0, 4)),
nt1: bytesToU32(resp.data.sublist(4, 8)),
par: bytesToU64(resp.data.sublist(8, 16)),
ks1: bytesToU64(resp.data.sublist(16, 24)),
par: bytesToBigU64(resp.data.sublist(8, 16)),
ks1: bytesToBigU64(resp.data.sublist(16, 24)),
nr: bytesToU32(resp.data.sublist(24, 28)),
ar: bytesToU32(resp.data.sublist(28, 32)));
}
Expand Down
31 changes: 26 additions & 5 deletions chameleonultragui/lib/bridge/dfu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'dart:async';
import 'package:chameleonultragui/helpers/general.dart';
import 'package:chameleonultragui/connector/serial_abstract.dart';
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import 'dart:math';

Expand Down Expand Up @@ -148,6 +149,8 @@ class DFUCommunicator {
_serialInstance = port;
}

List<int> _receiveBuffer = [];

Future<Uint8List?> sendCmd(DFUCommand cmd, Uint8List data) async {
var packet = Uint8List.fromList([cmd.value, ...data]);
if (!isBLE) {
Expand All @@ -159,14 +162,28 @@ class DFUCommunicator {
}

responseCompleter = Completer<List<int>>();
_receiveBuffer = [];

if (!_serialInstance!.isOpen) {
await _serialInstance!.open();
_serialInstance!.isOpen = true;
}

// we initialize completer each time in DFU, because it being recreated on each message
await _serialInstance!.registerCallback(responseCompleter?.complete);
await _serialInstance!.registerCallback((Uint8List chunk) {
if (responseCompleter == null || responseCompleter!.isCompleted) return;

if (isBLE) {
responseCompleter!.complete(chunk.toList());
return;
}

_receiveBuffer.addAll(chunk);
if (_receiveBuffer.contains(Slip.slipByteEnd)) {
responseCompleter!.complete(List<int>.from(_receiveBuffer));
_receiveBuffer = [];
}
});

log.d("Sending: ${bytesToHex(packet)}");
await _serialInstance!.write(packet);
Expand Down Expand Up @@ -244,6 +261,9 @@ class DFUCommunicator {

Future<Map<String, int>> calculateChecksum() async {
var response = await sendCmd(DFUCommand.calcChecSum, Uint8List(0));
if (response!.buffer.lengthInBytes < 8) {
response = await sendCmd(DFUCommand.calcChecSum, Uint8List(0));
}
var offset = ByteData.view(response!.buffer).getUint32(0, Endian.little);
var crc = ByteData.view(response.buffer).getUint32(4, Endian.little);

Expand All @@ -259,7 +279,7 @@ class DFUCommunicator {
for (var offset = 0; offset < firmwareBytes.length; offset += length) {
var tries = 0;
var crcBackup = crc;
for (; tries < ((Platform.isIOS) ? 50 : 10); tries++) {
for (; tries < ((!kIsWeb && Platform.isIOS) ? 50 : 10); tries++) {
await createObject(
objectType, min(firmwareBytes.length - offset, length));

Expand All @@ -283,7 +303,7 @@ class DFUCommunicator {
break;
}

if (tries == ((Platform.isIOS) ? 50 : 10)) {
if (tries == ((!kIsWeb && Platform.isIOS) ? 50 : 10)) {
throw ("Unable to recover from DFU");
}
}
Expand Down Expand Up @@ -325,6 +345,7 @@ class DFUCommunicator {
offset += toTransmit.length;
crc = calculateCRC32(toTransmit, crc) & 0xFFFFFFFF;
currentPrn++;

if (currentPrn == prn) {
await asyncSleep(1);
response = await calculateChecksum();
Expand All @@ -348,15 +369,15 @@ class DFUCommunicator {
offsetSize = 20;
}

if (Platform.isWindows || Platform.isMacOS || isBLE) {
if (kIsWeb || Platform.isWindows || Platform.isMacOS || isBLE) {
for (var offset = 0; offset < packet.length; offset += offsetSize) {
await _serialInstance!.write(
packet.sublist(
offset, offset + min(offsetSize, packet.length - offset)),
firmware: true);
}

if (isBLE && (Platform.isIOS || Platform.isMacOS)) {
if (kIsWeb || isBLE && (Platform.isIOS || Platform.isMacOS)) {
await asyncSleep(250);
}
} else {
Expand Down
4 changes: 4 additions & 0 deletions chameleonultragui/lib/connector/serial_abstract.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ abstract class AbstractSerial {
bool isDFU = false;
bool pendingConnection = false;
String portName = "None";
String name = "Abstract";
bool hasAllPermissions = true;
ConnectionType connectionType = ConnectionType.none;
dynamic messageCallback;
dynamic activeDevicePort;
VoidCallback? connectionStateCallback;

bool get isApiAvailable => true;

AbstractSerial({required this.log});

Future<bool> performConnect() async {
Expand Down
17 changes: 10 additions & 7 deletions chameleonultragui/lib/connector/serial_android.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:chameleonultragui/connector/serial_abstract.dart';
import 'package:chameleonultragui/connector/serial_ble.dart';
import 'package:chameleonultragui/connector/serial_mobile.dart';
import 'package:chameleonultragui/connector/serial_ble.dart' as ble;
import 'package:chameleonultragui/connector/serial_android_serial.dart'
as serial;
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';

// Class combines Android OTG and BLE serial
class AndroidSerial extends AbstractSerial {
late BLESerial bleSerial = BLESerial(log: log);
late MobileSerial mobileSerial = MobileSerial(log: log);
class SerialAdapter extends AbstractSerial {
late ble.SerialAdapter bleSerial = ble.SerialAdapter(log: log);
late serial.SerialAdapter mobileSerial = serial.SerialAdapter(log: log);
Future<bool>? permissionRequestFuture;
late bool hasAllPermissions = true;
@override
// ignore: overridden_fields
String name = "Android";

AndroidSerial({required super.log}) {
SerialAdapter({required super.log}) {
bleSerial.connectionStateCallback = notifyConnectionStateChanged;
mobileSerial.connectionStateCallback = notifyConnectionStateChanged;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import 'package:flutter/services.dart';
import 'package:usb_serial/usb_serial.dart';

// Class for Android Serial Communication
class MobileSerial extends AbstractSerial {
class SerialAdapter extends AbstractSerial {
Map<String, UsbDevice> deviceMap = {};
UsbPort? port;

MobileSerial({required super.log});
@override
// ignore: overridden_fields
String name = "Android USB";

SerialAdapter({required super.log});

@override
bool isManualConnectionSupported() {
Expand Down
8 changes: 6 additions & 2 deletions chameleonultragui/lib/connector/serial_ble.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Uuid dfuUUID = Uuid.parse("FE59");
Uuid dfuControl = Uuid.parse("8EC90001-F315-4F60-9FB8-838830DAEA50");
Uuid dfuFirmware = Uuid.parse("8EC90002-F315-4F60-9FB8-838830DAEA50");

class BLESerial extends AbstractSerial {
class SerialAdapter extends AbstractSerial {
FlutterReactiveBle flutterReactiveBle = FlutterReactiveBle();
QualifiedCharacteristic? txCharacteristic;
QualifiedCharacteristic? rxCharacteristic;
Expand All @@ -26,7 +26,11 @@ class BLESerial extends AbstractSerial {
Map<String, Chameleon> chameleonMap = {};
bool inSearch = false;

BLESerial({required super.log});
@override
// ignore: overridden_fields
String name = "BLE";

SerialAdapter({required super.log});

Future<List> availableDevices() async {
if (inSearch) {
Expand Down
4 changes: 4 additions & 0 deletions chameleonultragui/lib/connector/serial_emulator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import 'package:flutter/services.dart';
class EmulatorSerial extends AbstractSerial {
EmulatorSerial({required super.log});

@override
// ignore: overridden_fields
String name = "Emulator";

@override
Future<bool> performConnect() async {
return true;
Expand Down
Loading
Loading