Skip to main content

gicisky_lib 0.1.0 copy "gicisky_lib: ^0.1.0" to clipboard
gicisky_lib: ^0.1.0 copied to clipboard

A Flutter library for interacting with Gicisky BLE devices

Gicisky Flutter Library #

🔗 API Reference

A Flutter library for interacting with Gicisky BLE devices (e-paper and TFT displays). This library provides a backend-agnostic BLE interface, allowing you to use any BLE library of your choice while the library handles the Gicisky protocol, image processing, and device communication.

✨ Features #

  • Device Discovery - Scan for and parse Gicisky device advertisements
  • Device Information - Extract model, firmware, hardware, battery, and voltage data
  • Image Processing - Optimize, dither, and convert images for specific device models
  • Image Upload - Upload images to devices with real-time progress tracking
  • BLE Library Agnostic - Use any BLE library by implementing a simple interface
  • Multi-Platform - Works on Android, iOS, Linux, macOS, and Windows

📱 Supported Devices #

Model ID Device Type Size Colors
0x0B EPD 2.1" 250×122 Black/White/Red
0x33 EPD 2.9" 296×128 Black/White/Red
0x4B EPD 4.2" 400×300 Black/White/Red
0x2B EPD 7.5" 800×480 Black/White/Red
0xA0 TFT 2.1" 250×132 Black/White

📦 Installation #

Add this to your package's pubspec.yaml file:

dependencies:
  gicisky_lib: ^1.0.0

Then run:

flutter pub get

🚀 Quick Start #

Get up and running in just a few minutes:

import 'package:gicisky_lib/gicisky_lib.dart';
import 'package:gicisky_lib/src/ble/blue_plus_backend.dart';
import 'package:image/image.dart' as img;
import 'package:logging/logging.dart';

void main() async {
  // 1. Set up logging
  Logger.root.level = Level.ALL;
  Logger.root.onRecord.listen((record) {
    print('${record.loggerName}: ${record.message}');
  });

  // 2. Create BLE backend and protocol handler
  final ble = BluePlusBackend();
  final protocol = GiciskyProtocol(ble: ble);

  // 3. Scan for devices
  final devices = await ble.scanDevices();
  if (devices.isEmpty) {
    print('No devices found');
    return;
  }

  // 4. Connect to first device
  final deviceId = devices.keys.first;
  await ble.connect(deviceId);

  // 5. Process an image
  final image = img.decodeImage(File('my_image.png').readAsBytesSync())!;
  final deviceData = parseAdvertisement(devices[deviceId]!);
  final optimizedImage = optimize(image, deviceData.model);
  final giciskyBytes = convertToGiciskyBytes(optimizedImage, deviceData.model);

  // 6. Upload with progress tracking
  protocol.progressStream.listen((progress) {
    print('Progress: ${(progress.progress * 100).toStringAsFixed(1)}%');
  });

  await protocol.uploadImage(giciskyBytes);
  await ble.disconnect(); // Refreshes display

  print('Done!');
}

📖 Usage #

Basic Setup #

import 'package:gicisky_lib/gicisky_lib.dart';
import 'package:logging/logging.dart';

void main() {
  // Set up logging for debugging
  Logger.root.level = Level.ALL;
  Logger.root.onRecord.listen((record) {
    print('${record.loggerName}: ${record.message}');
  });
}

Using a Built-in BLE Backend #

Using BluePlusBackend

Add to pubspec.yaml:

dependencies:
  flutter_blue_plus: ^1.31.0
import 'package:gicisky_lib/gicisky_lib.dart';
import 'package:gicisky_lib/src/ble/blue_plus_backend.dart';

final ble = BluePlusBackend();
final protocol = GiciskyProtocol(ble: ble);

Using UniversalBleBackend

Although an universal_ble-based backend is available, it is not recommended. It does not work on Android.

Using Another BLE Backend #

If you want to use a BLE library other than flutter_blue_plus, you can create your own backend by implementing the BleInterface. See the BLE Library Agnostic Architecture section below for detailed instructions on how to implement a custom BLE backend.

Checking for Connected Devices #

Find all devices currently connected with the Gicisky service:

try {
  final connectedDevices = await ble.findConnected();
  
  if (connectedDevices.isEmpty) {
    print('No Gicisky devices are currently connected');
  } else {
    print('Found ${connectedDevices.length} connected device(s):');
    for (final mac in connectedDevices) {
      print('  - ${mac.toStringValue}');
    }
  }
} catch (e) {
  print('Check failed: $e');
}

Important Note: This method is particularly useful because connected devices typically don't advertise. If you're scanning for devices and can't find the ones you expect to be nearby, they might already be connected to your device or another app. You should disconnect from them before scanning:

// Check for connected devices first
final connectedDevices = await ble.findConnected();

if (connectedDevices.isNotEmpty) {
  print('Disconnecting from ${connectedDevices.length} device(s) before scanning...');
  for (final mac in connectedDevices) {
    try {
      await ble.connect(mac); // Connect to manage the device
      await ble.disconnect(); // Then disconnect
    } catch (e) {
      print('Failed to disconnect from $mac: $e');
    }
  }
}

// Now scan for devices
final devices = await ble.scanDevices();

Scanning for Devices #

Scan for Gicisky devices in the area:

try {
  final devices = await ble.scanDevices();
  
  print('Found ${devices.length} device(s):');
  for (final entry in devices.entries) {
    final mac = entry.key.toStringValue;
    final adv = entry.value;
    print('  - $mac: ${adv.name}');
  }
} catch (e) {
  print('Scan failed: $e');
}

Scan for a specific device by MAC address:

final mac = MacAddress.fromString('AA:BB:CC:DD:EE:FF');
final devices = await ble.scanDevices(mac: mac);

if (devices.isNotEmpty) {
  print('Found device: ${devices.keys.first}');
}

Parsing Device Information #

Extract detailed information from a Gicisky device advertisement:

final devices = await ble.scanDevices();
final deviceId = devices.keys.first;
final advertisement = devices[deviceId]!;

try {
  final deviceData = parseAdvertisement(advertisement);
  
  print('Device Name: ${deviceData.name}');
  print('Model: 0x${deviceData.model.toRadixString(16).padLeft(2, '0').toUpperCase()}');
  print('Firmware: ${deviceData.firmware}');
  print('Hardware: ${deviceData.hardware}');
  print('Battery: ${deviceData.battery}%');
  print('Voltage: ${deviceData.voltage}V');
} catch (e) {
  print('Failed to parse device data: $e');
}

Loading and Processing Images #

Process images for Gicisky devices in two steps:

// Prerequisites:
// - deviceData: from "Parsing Device Information" section above
// - image: loaded from file using your preferred image loading library
//   Example: final image = img.decodeImage(File('path/to/image.png').readAsBytesSync());

final model = deviceData.model;

// Step 1: Optimize the image for the device
// This includes:
// - Resizing to fit the display (with letterboxing to maintain aspect ratio)
// - Centering the image
// - Applying color quantization with Floyd-Steinberg dithering (by default)
// The output is an image you can display as a preview
final optimizedImage = optimize(image, model);

// Show the preview to the user (optional)
// displayImage(optimizedImage);

// Step 2: Convert to Gicisky byte format
// This handles device-specific formatting (mirroring, TFT transforms, compression, etc.)
// The image must be in device dimensions and proper color space
final giciskyBytes = convertToGiciskyBytes(
  optimizedImage,
  model
);

Image Processing Options #

The optimize function provides several options for controlling image processing:

// Use Floyd-Steinberg dithering (default - produces best results)
final optimizedImage = optimize(image, model, applyDithering: true);

// Use simple thresholding (no dithering) - allows custom thresholds
final optimizedImage = optimize(image, model, 
  applyDithering: false,
  lumThreshold: 100,  // Lower = more black pixels
  redThreshold: 150,  // Lower = more red pixels
);

Note: Threshold parameters are only used when applyDithering = false. When dithering is enabled (default), the dithering algorithm handles color quantization automatically.

Uploading an Image #

Complete example of connecting to a device and uploading an image:

// Prerequisites:
// - deviceId: from "Scanning for Devices" or "Parsing Device Information" sections above
// - imageData: GiciskyImageData from "Loading and Processing Images" section above

// Connect to device
await ble.connect(deviceId);

// Create protocol handler
final protocol = GiciskyProtocol(ble: ble);

// Subscribe to progress updates - OPTIONAL
final progressSubscription = protocol.progressStream.listen((progress) {
  print('${(progress.progress * 100).toStringAsFixed(1)}% - ${progress.message}');
});

try {
  // Upload the image
  await protocol.uploadImage(imageData);
  
  print('Upload complete!');
  
  // Disconnect (this refreshes the display)
  print('Disconnecting...');
  await ble.disconnect();
  
  print('Done! Display should now show the image.');
} finally {
  await progressSubscription.cancel();
}

Monitoring Upload Progress #

The GiciskyProtocol emits detailed progress updates through its progressStream:

final protocol = GiciskyProtocol(ble: ble);

protocol.progressStream.listen((progress) {
  switch (progress.stage) {
    case UploadStage.connecting:
      print('Connecting to device...');
      break;
    case UploadStage.preparing:
      print('Preparing upload: ${progress.message}');
      break;
    case UploadStage.uploading:
      print('Uploading: ${(progress.progress * 100).toStringAsFixed(1)}%');
      print('Bytes sent: ${progress.bytesSent}/${progress.totalBytes}');
      break;
    case UploadStage.complete:
      print('Upload complete!');
      break;
    case UploadStage.error:
      print('Error: ${progress.message}');
      break;
  }
});

Refreshing the Display Without Disconnecting #

If you want to keep the connection active after uploading (e.g., for multiple uploads), you can refresh the display programmatically:

// After upload is complete
await protocol.refreshDisplay(deviceId);
// Display is now refreshed and connection is still active

Note that this will still trigger a brief BLE disconnection + re-connection. This is a limitation of Gicisky firmware.

Handling Errors #

Always handle errors appropriately:

// Prerequisites:
// - deviceId: from "Scanning for Devices" or "Parsing Device Information" sections above
// - imageData: GiciskyImageData from "Loading and Processing Images" section above

try {
  await ble.connect(deviceId);
  await protocol.uploadImage(imageData);
  await ble.disconnect();
} catch (e) {
  print('Operation failed: $e');
  
  // Attempt to disconnect on error
  try {
    await ble.disconnect();
  } catch (_) {
    print('Could not disconnect');
  }
}

✅ Testing #

This library includes comprehensive unit tests covering all major features.
To run all tests:

flutter test

To run a specific test file:

flutter test test/types_test.dart

To run tests with coverage:

flutter test --coverage
genhtml coverage/lcov.info -o coverage/html

The test suite currently includes 90+ tests covering core functionality and edge cases.

📱 Platform-Specific Requirements #

Android #

Add to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

iOS #

Add to ios/Runner/Info.plist:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app needs Bluetooth access to communicate with Gicisky devices.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app needs Bluetooth access to communicate with Gicisky devices.</string>

Note that iOS is not tested.

Linux #

Ensure your user has permission to access Bluetooth:

sudo usermod -a -G bluetooth $USER

👨‍⚕️ Troubleshooting #

Scan finds no devices #

  • Ensure Bluetooth is enabled
  • Check that location services are enabled (required for BLE scanning on Android)
  • Verify that the device is in advertising mode (not connected to another device!)
  • Make sure the device is within range (typically < 10 meters)
  • Restart Bluetooth

Connection fails #

  • Verify the device is not already connected to another app
  • Check that the device advertises the Gicisky service UUID: 0000fef0-0000-1000-8000-00805f9b34fb
  • Ensure proper permissions are granted
  • Try disconnect and scan again

Upload never ends #

  • This is a known issue on Android with Universal BLE Backend
  • Switch to Blue Plus Backend

Display doesn't refresh after upload #

  • This is normal if you don't disconnect - the device hardware requires disconnection to refresh
  • Either disconnect after upload, or use refreshDisplay()
  • Wait a few seconds after disconnection for the e-ink panel to update

MIT license.
See the LICENSE file for details.

Gicisky is a trademark of Wuhan Hi-Link Electronic Co.,Ltd. This project is a third-party implementation of their protocol, released as-is, without any kind of warranty to the fullest extent of applicable law. Neither the developers of this project nor Wuhan Hi-Link Electronic Co.,Ltd. are to be held responsible for damages caused by this software.

🤝 Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

📧 Support #

For issues, questions, or contributions, please visit the project repository.

0
likes
115
points
100
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter library for interacting with Gicisky BLE devices

Homepage

License

MIT (license)

Dependencies

flutter, flutter_blue_plus, image, logging, universal_ble

More

Packages that depend on gicisky_lib