Author Topic: Digispark Tiny and serial access from Android  (Read 4793 times)

ralfoide

  • Newbie
  • *
  • Posts: 2
Digispark Tiny and serial access from Android
« on: January 21, 2015, 06:08:22 pm »
I recently needed to control a Digispark tiny from an Android tablet or phone via OTG using a simple serial access via DigiUSB. I haven't quite seen any Android code posted for this and would like to simply post what I've used, in case it help others.

The first part of the answer is to discover the Digispark device on USB.
This can be easily be done as follows using the Android UsbManager API:

Code: [Select]
private static final int DIGISPARK_VID = 0x16C0;
private static final int DIGISPARK_PID = 0x05DF;

private void listDevices() {
    UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
    HashMap<String, UsbDevice> devices = manager.getDeviceList();
    for (UsbDevice device : devices.values()) {
        if (device.getVendorId() == DIGISPARK_VID && device.getProductId() == DIGISPARK_PID) {
            mDevicesList.add(device);
        }
    }
}

You can add the device to an ArrayList backed ListView or RecyclerView or use it directly.
Since we're talking about an Android phone or tablet, in most cases there won't be any hub involved so you can pretty much stop iterating after you find one.

An important detail to remember is that a Digispark Tiny exposes 16D0 / 0753 as vid/pid during its 5 first seconds (for the programming bootstrap) and the VID/PID changes after. This can be ignored, except: if that combo is seen more than 5 seconds, we're probably dealing with a Digispark that hasn't been programmed yet.

Now one trick is that once you find an UsbDevice instance, you can't quite use it right away. You need to ask permission to the user to use it. This is done by sending a PendingIntent. Android displays a dialog box asking for permission to the user and once granted you get the intent back. You can use any kind of BroadcastReceiver and in my case the easiest thing was to send it to my own activity:
Code: [Select]
private static final int REQ_USB_PERMISSION = 1;

public void onUserClickedOnDeviceEntry(@NonNull UsbDevice device) {
    UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);

    Intent in = new Intent(context, MyActivity.class);
    Uri u = new Uri.Builder().scheme("usb").path(device.getDeviceName()).build();
    in.setData(u);
    in.putExtra("device", device);  // Parcel
    PendingIntent pi = PendingIntent.getActivity(context, REQ_USB_PERMISSION, in, PendingIntent.FLAG_UPDATE_CURRENT);
    manager.requestPermission(device, pi);
}

Note that a PendingIntent must be unique and the extra data is ignored for that purpose. Using the device usb path as a URI is a convenient way to make a unique intent for that specific device. On the receiver side we could lookup the usb device path again but it's just easier to parcel the device info into the intent as extra data.

And once the user grants the permission:
Code: [Select]
@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    if (intent != null && intent.getData() != null && "usb".equals(intent.getData().getScheme())) {
        UsbDevice device = intent.getParcelableExtra("device");
        if (device != null) {
            onDeviceSelected(device);
        }
    }
}

OK now we have a device and we have permission to use it. So how do we use it?
That's where I stumbled quite a bit at first. I started digging into the USB protocol and interfaces, end-points and control vs bulk messages, then I realized I had the answer right under my nose: what does the Windows DDK usbview report about a digispark? And what does digiusb.c, the little C client provided to communicate on the desktop, does?

Pro tip: if you want to know how an USB device is structured, grab the Windows Kits 8.1 and run Debuggers\x86\usbview.exe; that will list you all the interfaces and endpoints of a device.

In this case the Digispark Tiny has one interface and one end-point. There's a stack-overflow linked in another post that makes a bit deal of iterating through interfaces and end-points and figuring their direction. It turns out this information is actually quite irrelevant. We just don't need any of this with the Digispark Tiny!

To understand why, simply look at the little digiusb C program that comes with the Digispark. What does it do?

From digiusb/send.cpp, it writes character per character using control messages:
Code: [Select]
devHandle = usb_open(digiSpark);
numInterfaces = digiSpark->config->bNumInterfaces; // ⇒ 1
interface = &(digiSpark->config->interface[0].altsetting[0]); // ⇒ 0
/* result = usb_claim_interface(devHandle, interface->bInterfaceNumber); */
result = usb_control_msg(devHandle, (0x01 << 5), 0x09, 0, argv[1][i], 0, 0, 1000); // * N
result = usb_control_msg(devHandle, (0x01 << 5), 0x09, 0, '\n', 0, 0, 1000); // EOL
result = usb_release_interface(devHandle, interface->bInterfaceNumber);
usb_close(devHandle);

The interface is used to claim and release it, however the claim part is commented out in the code and the interface itself is never used to send data.

From digiusb/receive.cpp:
Code: [Select]
Loop:
theChar = 4
result = usb_control_msg(devHandle, (0x01 << 5) | 0x80, 0x01, 0, 0, &thechar, 1, 1000);
if result < 0: break;
if theChar == 4 (not changed): break;

From linux usb.h:
Code: [Select]
int usb_control_msg(usb_dev_handle *dev, int requesttype, int request, int value, int index, char *bytes, int size, int timeout);
  • requestType: 1<<5=x20 for send,1<<5+x80=xA0 for receive.
    1<<5 = USB_TYPE_CLASS;  0x80=USB_DIR_IN, 0=USB_DIR_OUT
  • request: 9 for send, 1 for receive.
  • value: 0
  • index: char to send, 0 for receive.
  • bytes ptr: null for send, &char for receive.
  • bytes len: 0 for send, 1 for receive.
  • timeout: 1000.

So what's going on here? It is using "control messages", a mechanism from the USB spec done to communicate with a device directly without having a proper channel (based on my cursory very limited reading of the spec.) And by default control messages are sent to the end-point 0. So basically we can ignore we have a HID compliant device and since we know it's a Digispark, we can hack away.

I've listed the usb_control_msg API above from usb.h because, you've probably guessed it, Android has exactly the same function, argument for argument:
Code: [Select]
public static final int REQ_OUT = UsbConstants.USB_TYPE_CLASS + UsbConstants.USB_DIR_OUT;
public static final int REQ_IN = UsbConstants.USB_TYPE_CLASS + UsbConstants.USB_DIR_IN;

UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
UsbDeviceConnection cnx = manager.openDevice(device);

// Send character 'A'
cnx.controlTransfer(REQ_OUT, 9, 0, 'A', null, 0, 1000);

// Read a character
byte[] buffer = new byte[16];
buffer[0] = 4;
res = cnx.controlTransfer(REQ_IN, 1, 0, 0, buffer, 1, 1000);
char c = (char) buffer[0];
if (res >= 0 && c != 4) ... use character c

Obviously since you're reading character by character, you need to deal with \n if they are important for your application. Also you don't want to do that work on the app main UI thread -- always use a thread, or even easier an AsyncTask to do the work.

Here's an example of usage from something that blinks the led of the digispark and reads a value from the ADC:
Code: [Select]
private class CalibrationTask extends AsyncTask<UsbDevice, Void, Void> {
    private int mMin;
    private int mMax;

    @Override
    protected Void doInBackground(UsbDevice... params) {
        UsbDevice device = params[0];
        mMin = 1023;
        mMax = 0;
        publishProgress();

        UsbManager manager = (UsbManager) MyActivity.this.getSystemService(Context.USB_SERVICE);
        UsbDeviceConnection cnx = manager.openDevice(device);
        // handle openDevice failure as needed for your application

        while (!isCancelled()) {
            try {
                Thread.sleep(250 /*ms*/);
                doBlink(cnx);
                int v = readValueSync(cnx);
                if (v >= 0 && v <= 1023) {
                    if (v < mMin) mMin = v;
                    if (v > mMax) mMax = v;
                }
                publishProgress();
            } catch (InterruptedException e) {
                return null; // interrupted on cancel
            }
        }
        return null;
    }

    @Override
    protected void onProgressUpdate(Void... values) {
        super.onProgressUpdate(values);
        // update the UI here;
    }
}

public static final int REQ_OUT = UsbConstants.USB_TYPE_CLASS + UsbConstants.USB_DIR_OUT;
public static final int REQ_IN = UsbConstants.USB_TYPE_CLASS + UsbConstants.USB_DIR_IN;

private void doBlink(@NonNull UsbDeviceConnection cnx) {
    cnx.controlTransfer(REQ_OUT, 9, 0, 'b', null, 0, 1000);
    cnx.controlTransfer(REQ_OUT, 9, 0, '\n', null, 0, 1000);
}

/** Reads digispark value. Blocks till gets the reply. */
private int readValueSync(@NonNull UsbDeviceConnection cnx) throws InterruptedException {
    // send t + \n
    cnx.controlTransfer(REQ_OUT, 9, 0, 't', null, 0, 1000);
    cnx.controlTransfer(REQ_OUT, 9, 0, '\n', null, 0, 1000);

    // read numbers back till we get \n
    byte[] buffer = new byte[16];
    int value = 0;
    int res;
    while (true) {
        buffer[0] = 4;
        res = cnx.controlTransfer(REQ_IN, 1, 0, 0, buffer, 1, 1000);
        char c = (char) buffer[0];
        if (res < 0 || c == 4) {
            return -1;
        }
        if (c == '\n') break;
        if (Character.isDigit(c)) {
            value = value * 10 + (c - '0');
        }
    }

    return value;
}

private void onDeviceSelected(@Nullable UsbDevice device) {
    mCalibTask = new CalibrationTask();
    mCalibTask.execute(device);
}

and the corresponding Digispark program:
Code: [Select]
#include <DigiUSB.h>

#define LED     1    // on-board led on digital pin 1
#define ADC1    1    // ADC channel 1
#define ADC1_P2 2    // ADC1 is connected to digital pin 2

byte in = 0;
int value = 0;  // ADC value is 10-bits
char buf[12];   // "-2147483648\0" = 12 characters.

void setup() {
  DigiUSB.begin();
  pinMode(LED,     OUTPUT);
  pinMode(ADC1_P2, INPUT);
  blink();
}

void blink() {
  digitalWrite(LED, HIGH);
  delay(240 /*ms*/);
  digitalWrite(LED, LOW);
}

void loop() {
  DigiUSB.refresh();
  if (DigiUSB.available() > 0) {
    in = DigiUSB.read();
    if (in == 't') {
      trigger();
    } else if (in == 'b') {
      blink();
    }
  }
}

void trigger() {
  // Read 10-bit ADC value and output to usb
  // Take average of 4 reads
  // 10-bits = 1024.
  value  = analogRead(ADC1);
  value += analogRead(ADC1);
  value += analogRead(ADC1);
  value += analogRead(ADC1);
  value /= 4;
  buf[5] = 0;
  buf[4] = '0'+ (value % 10);    // 123x
  value /= 10;
  buf[3] = '0'+ (value % 10);    // 12x4
  value /= 10;
  buf[2] = '0'+ (value % 10);    // 1x34
  value /= 10;
  buf[1] = '0'+ (value % 10);    // x234
  value /= 10;
  buf[0] = '0'+ (value % 10);    // x1234 -- should be zero
  DigiUSB.println(buf);
}

I hope that will be useful to some of you.

alberio

  • Newbie
  • *
  • Posts: 1
Re: Digispark Tiny and serial access from Android
« Reply #1 on: July 24, 2016, 09:48:02 pm »
byte[] buffer = new byte[16];                 
                  while (true) {
                      buffer[0] = 4;
                      int res = cnx.controlTransfer(USB_CONTROL_IN, 1, 0, 0, buffer, 1, 2000);
                      char c = (char) buffer[0];
                      retorno = ""+c;
                      if (res < 0 || c == 4) {                         
                         break;                                          
                      }
                      if (c == '\n') break;
                                           
                     }

not working!