Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add web support #120

Closed
wants to merge 62 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
6e2eafc
feat: add support for web
pimlie Aug 19, 2023
e4d5949
Merge branch 'main' into feat-add-web-support
pimlie Aug 19, 2023
a1ec39f
chore: revert unneeded change
pimlie Aug 19, 2023
53f5982
fix: tests
pimlie Aug 19, 2023
d685750
chore: remove debug logs
pimlie Aug 19, 2023
9325ae0
refactor: refactor file picking & saving to support web
pimlie Aug 19, 2023
4f7dadf
feat: support key recovery using dictionary
pimlie Aug 19, 2023
bc23bdd
feat: set portName for web serial
pimlie Aug 19, 2023
a1f36a0
fix: if releases is a list then containsKey doesnt exists
pimlie Aug 20, 2023
ebbe746
chore: rename preform to perform
pimlie Aug 20, 2023
113b31a
fix: set disabled on nav items instead of manually changing text color
pimlie Aug 20, 2023
d9918b2
fix: dont show reader mode toggle for chameleon lites
pimlie Aug 20, 2023
3574b82
feat: show generic/both chameleon devices on connect page instead of …
pimlie Aug 20, 2023
a559778
fix: refactor reading data from serial connection
pimlie Aug 20, 2023
6dea3ea
feat: update favicons
pimlie Aug 20, 2023
eac89a8
fix: remove old code
pimlie Aug 20, 2023
cbdecdb
feat: dont try to update the fw automatically on web and show an unsu…
pimlie Aug 20, 2023
d672cfb
Merge branch 'main' into feat-add-web-support
Foxushka Aug 20, 2023
10ce49e
Correct merge
Foxushka Aug 20, 2023
0116136
chore: add some improvements to try to prevent hanging on device disc…
pimlie Aug 20, 2023
b3ed064
Merge remote-tracking branch 'refs/remotes/origin/feat-add-web-suppor…
pimlie Aug 20, 2023
baaabf9
feat: refactor chameleon ultra detection
pimlie Aug 20, 2023
44f1036
feat: add device name getter on abstract serial
pimlie Aug 20, 2023
c6a0b11
feat: make firmware flashing using zip work on web
pimlie Aug 20, 2023
ba7ceeb
feat: display dfu specific cards on connect page with both flash late…
pimlie Aug 22, 2023
a7066de
Merge branch 'main' into feat-add-web-support
pimlie Aug 22, 2023
9a1f59d
chore: resolve 'dart fix' issues
pimlie Aug 22, 2023
2cb5d56
fix: remove main app bar
pimlie Aug 22, 2023
82775df
fix: move mode & settings icons back to bottom on home
pimlie Aug 22, 2023
6151727
fix: revert moving actions to appbar on home & connect page
pimlie Aug 22, 2023
05b5cdc
fix: resolve merge issue, use custom tag color
pimlie Aug 22, 2023
f302fe5
fix: dark mode cards
pimlie Aug 22, 2023
6dbd85b
fix: extract correct sak/atqa for uid7 cards
pimlie Aug 22, 2023
81f499b
Merge branch 'main' into feat-add-web-support
pimlie Aug 24, 2023
0b3d493
chore: update readme
pimlie Aug 24, 2023
82bdbed
fix: performDisconnect should not be protected
pimlie Aug 24, 2023
ed3a1d4
fix: add some page headers back
pimlie Aug 24, 2023
2cb9106
chore: remove unneeded import
pimlie Aug 24, 2023
8bcd7dc
feat: add a/b button mode back to device settings dialog
pimlie Aug 24, 2023
8e0ae33
feat: add poc for cloudflare proxy
pimlie Aug 25, 2023
d6178f7
feat: refactor firmware flashing using composition
pimlie Aug 26, 2023
106960a
chore: remove duplicate layout builder
pimlie Aug 26, 2023
4cdd6f6
fix: disable/remove auto rebuilding on screen resize
pimlie Aug 26, 2023
4bafe48
chore: remove unused dependency
pimlie Aug 26, 2023
05dc1d0
fix: add reset factory settings back
pimlie Aug 26, 2023
fa283b5
chore: update readme
pimlie Aug 26, 2023
1583a4b
fix: add showConfirmDialog helper to ensure proper return type preven…
pimlie Aug 26, 2023
adc95ca
Merge branch 'main' into feat-add-web-support
pimlie Aug 27, 2023
3b671d1
Merge branch 'main' into feat-add-web-support
pimlie Aug 27, 2023
dde2427
fix: merge issues
pimlie Aug 27, 2023
2743a6b
chore: remove old/unused file
pimlie Aug 27, 2023
0b04957
fix: dont use gridview on connect page when no results
pimlie Aug 27, 2023
b940c37
fix: update instructions for updating firmware on web after entering dfu
pimlie Aug 27, 2023
a279d33
fix: add missing scrollview back in device settings
pimlie Aug 27, 2023
aeb4ebf
fix: add viaBLE prop back & improve error handling during firmware flash
pimlie Aug 27, 2023
55a6816
fix: try fix crc error by increasing receive timeout
pimlie Aug 27, 2023
5be7cd1
fix: only use async/sleep for web
pimlie Aug 27, 2023
226755b
fix: change button type
pimlie Aug 28, 2023
c054a9a
feat: ask user which device type is connected if unknown and user wan…
pimlie Aug 28, 2023
7f88aca
feat: add setting so users can disable mobile/vertical navigation
pimlie Aug 28, 2023
2a1be2d
chore: wording
pimlie Aug 28, 2023
db49a85
chore: switch sizer dependency
pimlie Aug 28, 2023
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ 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 users:

You need to pair your Chameleon first before it shows up on the connect page, click on the handshake icon and select the relevant serial devices.

*Known issues*
- Downloading firmware assets requires the use of a CORS proxy. This project provides one but you can also set one yourself
- On the connect page there is no indication if the connected device is a Chameleon Ultra or Lite. After connecting to a device the correct type is displayed
> This is because the Web Serial API is quite limited with the [device information](https://developer.mozilla.org/en-US/docs/Web/API/SerialPort/getInfo) it returns as it only returns an usb vendor id & product id (which are the same for Ultra's & Lite's). So on the connect page any device will be displayed as just Chameleon, after you connect to a specific device the correct device type will be detected
- blackside key recovery is not supported (yet)

## Contributing
Contributions are welcome, most stuff that needs to be done can either be found in our [issues](https://github.com/GameTec-live/ChameleonUltraGUI/issues) or on the [Project board](https://github.com/users/GameTec-live/projects/2)

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added chameleonultragui/fonts/RobotoMono-Regular.ttf
Binary file not shown.
116 changes: 81 additions & 35 deletions chameleonultragui/lib/bridge/chameleon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ enum ChameleonCommand {
factoryReset(1020), // WARNING: ERASES ALL

// button config
getButtonPressConfig(1026),
setButtonPressConfig(1027),
getLongButtonPressConfig(1028),
setLongButtonPressConfig(1029),
getButtonConfigConfig(1026),
setButtonConfigConfig(1027),
getLongButtonConfigConfig(1028),
setLongButtonConfigConfig(1029),

// hf reader commands
scan14ATag(2000),
Expand Down Expand Up @@ -93,18 +93,16 @@ enum ChameleonCommand {
final int value;
}

enum TagType {
unknown(0),
em410X(1),
mifareMini(2),
mifare1K(3),
mifare2K(4),
mifare4K(5),
ntag213(6),
ntag215(7),
ntag216(8);

const TagType(this.value);
enum ChameleonCommandStatus {
paramError(96),
deviceModeError(102),
invalidCommand(103),
deviceSuccess(104),
notImplemented(105),
flashWriteFail(112),
flashReadFail(113);

const ChameleonCommandStatus(this.value);
final int value;
}

Expand All @@ -117,6 +115,24 @@ enum TagFrequency {
final int value;
}

enum TagType {
unknown(0, 'Unknown', TagFrequency.unknown, false),
em410X(1, 'EM410X', TagFrequency.lf, true),
mifareMini(2, 'Mifare Mini', TagFrequency.hf, true),
mifare1K(3, 'Mifare Classic 1K', TagFrequency.hf, true),
mifare2K(4, 'Mifare Classic 2K', TagFrequency.hf, true),
mifare4K(5, 'Mifare Classic 4K', TagFrequency.hf, true),
ntag213(6, 'NTAG213', TagFrequency.hf, false),
ntag215(7, 'NTAG215', TagFrequency.hf, false),
ntag216(8, 'NTAG216', TagFrequency.hf, false);

const TagType(this.value, this.name, this.frequency, this.writable);
final int value;
final String name;
final TagFrequency frequency;
final bool writable;
}

enum AnimationSetting {
full(0),
minimal(1),
Expand Down Expand Up @@ -332,6 +348,7 @@ class ChameleonCommunicator {
data: Uint8List.fromList(dataResponse));
log.d(
"Received message: command = ${message.command}, status = ${message.status}, data = ${bytesToHex(message.data)}");

dataPosition = 0;
dataBuffer = [];
messageQueue.add(message);
Expand All @@ -358,17 +375,23 @@ class ChameleonCommunicator {
_serialInstance!.isOpen = true;
}

log.d("Sending: ${bytesToHex(dataFrame)}");
log.d("Sending $cmd (${cmd.value}), data: ${bytesToHex(dataFrame)}");

var success = false;
try {
success = await _serialInstance!.write(Uint8List.fromList(dataFrame));
} catch (_) {
rethrow;
}

if (!success) {
throw ("Write failed for $cmd (${cmd.value})");
}

if (skipReceive) {
try {
await _serialInstance!.write(Uint8List.fromList(dataFrame));
} catch (_) {}
return null;
}

await _serialInstance!.write(Uint8List.fromList(dataFrame));

while (true) {
for (var message in messageQueue) {
if (message.command == cmd.value) {
Expand All @@ -377,9 +400,9 @@ class ChameleonCommunicator {
}
}

if (startTime.millisecondsSinceEpoch + timeout.inMilliseconds <
DateTime.now().millisecondsSinceEpoch) {
throw ("Timeout waiting for response for command ${cmd.value}");
var msSinceStart = DateTime.now().millisecondsSinceEpoch - startTime.millisecondsSinceEpoch;
if (msSinceStart >= timeout.inMilliseconds) {
throw ("Timeout after ${msSinceStart}ms waiting for response for command $cmd (${cmd.value})");
}

await asyncSleep(1);
Expand All @@ -397,12 +420,29 @@ class ChameleonCommunicator {
Future<int> getFirmwareVersion() async {
var resp = await sendCmd(ChameleonCommand.getAppVersion);
if (resp!.data.length != 2) throw ("Invalid data length");
return (resp.data[1] << 8) | resp.data[0];
final result = (resp.data[1] << 8) | resp.data[0];
return result;
}

Future<String> getDeviceChipID() async {
var resp = await sendCmd(ChameleonCommand.getDeviceChipID);
return bytesToHex(resp!.data);
final result = bytesToHex(resp!.data);
return result;
}

Future<bool> detectChameleonUltra() async {
// Check if device is a ChameleonUltra by trying to write a LF tag but
// with empty parameters.
//
// A Chameleon Ultra will return either:
// - STATUS_PAR_ERR = 0x60
// - STATUS_DEVICE_MODE_ERROR = 0x66
//
// A Chameleon Lite will return
// - STATUS_INVALID_CMD = 0x67
//
var result = await sendCmd(ChameleonCommand.writeEM410XtoT5577);
return result!.status != ChameleonCommandStatus.invalidCommand.value;
}

Future<String> getDeviceBLEAddress() async {
Expand Down Expand Up @@ -740,16 +780,17 @@ class ChameleonCommunicator {

Future<int> getActiveSlot() async {
// get the selected slot on the device, 0-7 (8 slots)
return (await sendCmd(ChameleonCommand.getActiveSlot))!.data[0];
var resp = (await sendCmd(ChameleonCommand.getActiveSlot));
return resp!.data[0];
}

Future<List<(TagType, TagType)>> getUsedSlots() async {
List<(TagType, TagType)> tags = [];
var resp = await sendCmd(ChameleonCommand.getSlotInfo);
for (var i = 0; i < 8; i++) {
tags.add((
numberToChameleonTag(resp!.data[(i * 2)]),
numberToChameleonTag(resp.data[(i * 2) + 1])
numberToTagType(resp!.data[(i * 2)]),
numberToTagType(resp.data[(i * 2) + 1])
));
}
return tags;
Expand Down Expand Up @@ -845,9 +886,14 @@ class ChameleonCommunicator {
}

Future<ButtonConfig> getButtonConfig(ButtonType type) async {
var resp = await sendCmd(ChameleonCommand.getButtonPressConfig,
var resp = await sendCmd(ChameleonCommand.getButtonConfigConfig,
data: Uint8List.fromList([type.value]));
if (resp!.data[0] == 1) {

if (resp!.data.isEmpty) {
return ButtonConfig.disable;
}

if (resp.data[0] == 1) {
return ButtonConfig.cycleForward;
} else if (resp.data[0] == 2) {
return ButtonConfig.cycleBackward;
Expand All @@ -859,12 +905,12 @@ class ChameleonCommunicator {
}

Future<void> setButtonConfig(ButtonType type, ButtonConfig mode) async {
await sendCmd(ChameleonCommand.setButtonPressConfig,
await sendCmd(ChameleonCommand.setButtonConfigConfig,
data: Uint8List.fromList([type.value, mode.value]));
}

Future<ButtonConfig> getLongButtonConfig(ButtonType type) async {
var resp = await sendCmd(ChameleonCommand.getLongButtonPressConfig,
var resp = await sendCmd(ChameleonCommand.getLongButtonConfigConfig,
data: Uint8List.fromList([type.value]));
if (resp!.data[0] == 1) {
return ButtonConfig.cycleForward;
Expand All @@ -878,7 +924,7 @@ class ChameleonCommunicator {
}

Future<void> setLongButtonConfig(ButtonType type, ButtonConfig mode) async {
await sendCmd(ChameleonCommand.setLongButtonPressConfig,
await sendCmd(ChameleonCommand.setLongButtonConfigConfig,
data: Uint8List.fromList([type.value, mode.value]));
}
}
40 changes: 33 additions & 7 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' show kIsWeb;
import 'package:logger/logger.dart';
import 'dart:math';

Expand Down Expand Up @@ -166,12 +167,37 @@ class DFUCommunicator {
}

// we initialize completer each time in DFU, because it being recreated on each message
await _serialInstance!.registerCallback(responseCompleter?.complete);
List<int>? readBuffer = List<int>.empty(growable: true);
await _serialInstance!.registerCallback((List<int> bytes) {
if (kIsWeb) {
readBuffer?.addAll(bytes);
} else {
responseCompleter?.complete(bytes);
}
});

log.d("Sending: ${bytesToHex(packet)}");
await _serialInstance!.write(packet);

List<int>? readBuffer = await responseCompleter?.future;
if (kIsWeb) {
// Use a while loop to ensure the serial port stream has been
// fully drained. E.g. on web there is no good way to check if
// the stream is empty but the stream might consist of more then
// one chunk, even if the chunk is only a couple bytes
var bufferSize = readBuffer.length;
while(true) {
await asyncSleep(10);

// Stop checking for more data
if (bufferSize > 0 && bufferSize == readBuffer.length) {
break;
}

bufferSize = readBuffer.length;
}
} else {
readBuffer = await responseCompleter?.future;
}

if (readBuffer == null || readBuffer.isEmpty) {
return null;
Expand Down Expand Up @@ -339,12 +365,12 @@ class DFUCommunicator {
Future<void> delayedSend(Uint8List packet) async {
// Windows has some issues with transmitting data
// We work around it by sending message by parts with delay
var offsetSize = 128;
if (isBLE) {
offsetSize = 20;
}
if (!kIsWeb && (Platform.isWindows || Platform.isMacOS || isBLE)) {
var offsetSize = 128;
if (isBLE) {
offsetSize = 20;
}

if (Platform.isWindows || Platform.isMacOS || isBLE) {
for (var offset = 0; offset < packet.length; offset += offsetSize) {
await _serialInstance!.write(
packet.sublist(
Expand Down
47 changes: 43 additions & 4 deletions chameleonultragui/lib/connector/serial_abstract.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import 'dart:typed_data';
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';

enum ChameleonDevice { none, ultra, lite }
// ChameleonDevice.unknown means we know the device is a Chameleon just not whether its an ultra or lite
enum ChameleonDevice {
none('None'),
unknown('Chameleon'),
ultra('Chameleon Ultra'),
lite('Chameleon Lite');

const ChameleonDevice(this.name);
final String name;
}

enum ConnectionType { none, usb, ble }

Expand All @@ -18,6 +29,25 @@ class Chameleon {
required this.dfu});
}

enum ChameleonVendor {
proxgrind(0x6868),
dfu(0x1915);

const ChameleonVendor(this.value);
final int value;
}

bool isChameleonVendor(int? vendorId, [ChameleonVendor? vendor, List<ChameleonVendor>? vendors ]) {
if (vendors == null && vendor != null) {
vendors = [vendor];
}

vendors ??= ChameleonVendor.values;
return vendors.any((id) {
return id.value == vendorId;
});
}

class AbstractSerial {
Logger log = Logger();
ChameleonDevice device = ChameleonDevice.none;
Expand All @@ -26,16 +56,25 @@ class AbstractSerial {
bool isDFU = false;
String portName = "None";
ConnectionType connectionType = ConnectionType.none;
dynamic messageCallback;
void Function(List<int>)? messageCallback;

Future<bool> performConnect() async {
return false;
}

@mustCallSuper
Future<bool> performDisconnect() async {
// Reset state of connected device
isOpen = false;
// connected = false; // TODO: should this be unset here too or in child implementations?
device = ChameleonDevice.none;
connectionType = ConnectionType.none;
messageCallback = null;
return false;
}

Future pairDevices() async {} // For web only

Future<List> availableDevices() async {
return [];
}
Expand All @@ -54,7 +93,7 @@ class AbstractSerial {
return false;
}

Future<void> registerCallback(dynamic callback) async {
Future<void> registerCallback(void Function(List<int>)? callback) async {
messageCallback = callback;
}
}
Loading