pimoroni / trackball-python

Python library for the Pimoroni Track Ball Breakout
MIT License
38 stars 6 forks source link

Arduino support? #5

Open mfxpyro1 opened 4 years ago

mfxpyro1 commented 4 years ago

You specifically advertise this as being "perfect for adding navigation or control to your Raspberry Pi or Arduino projects." Yet you have zero Arduino support for it, not even a basic listing of I2C registers and how to use them unless you're prepared to reverse engineer the python library. An Arduino library would be nice but in the absence of that could you as least document the protocol!

Gadgetoid commented 4 years ago

To be frank the Python library is 133 lines of code, very concise and is about the most accurate documentation you're likely to see. I can't spare the time to transcribe it into text and I think it'd be a wasted effort since anyone could- I'm pretty certain- find & replace over the Python code to produce a half decent Arduino library.

I mean the registers are all there at the top: https://github.com/pimoroni/trackball-python/blob/b45e5c6fdb73ddccd443c0f1c069956b7f6fe77a/library/trackball/__init__.py#L9-L40

The only unusual part is the 5 registers that describe left/right/up/down/switch- these contain values describing the amount of left/right/up/down the ball has moved since the last read so X is right-left and Y is down-up.

The switch register has a 7bit count of the number of presses there have been since the last update, and the MSB (MSK_SWITCH_STATE) indicates the current switch state (pressed/released).

Right, now to find a guillotine and whoever mentioned Arduino in the product description! (those two things are totally unrelated honest)

wez commented 3 years ago

@Gadgetoid would you mind describing how the interrupt pin is intended to be used? Is the intent that reading the trackball state clears the interrupt pin, or is the application expected to update the interrupt register after reading the state? Is the interrupt pin high or low when the interrupt register triggered flag is set? Could you also briefly describe what the control fread and fwrite flags are for?

Gadgetoid commented 3 years ago

The interrupt pin is active-low and, from what I can tell, is cleared on read.

I haven't the foggiest clue what FREAD and FWRITE are or were, but they might have been a vestige of when flash IO was a little more transparent to the user. I'd say you can comfortably ignore them.

It's on our TODO list to write a Pico SDK driver for this board, so perhaps that will provide a better reference for Arduino code.

wez commented 3 years ago

Thanks; the main reason I ask about the interrupts is that the device seems to continually assert or toggle (I haven't gotten around to printf debugging precisely which) the interrupt status even when there is no movement on the trackball; I wonder if there is a mode to adjust this; perhaps the sleep control can help?

wez commented 3 years ago

... I had mis-transcribed the interrupt register as 0x9 instead of 0xf9 and now things make more sense!

Gadgetoid commented 3 years ago

This is very much an odd duck of a breakout- I appreciate you taking the time to grapple with it!

ShiftedMr commented 3 years ago

@wez any luck working with the trackball? I'm trying to figure out my first i2c project with it and an stm32duino (bluepill) If you've pushed your code anywhere do you mind if I take a look?

Gadgetoid commented 3 years ago

If you shim I2C with your own implementation, or just replace the function calls, then you should be able to use this code: https://github.com/pimoroni/pimoroni-pico/blob/0bda2abd2acd4269725efb81ffd3f33621a8e176/drivers/trackball/trackball.cpp

ShiftedMr commented 3 years ago

If you shim I2C with your own implementation, or just replace the function calls, then you should be able to use this code: https://github.com/pimoroni/pimoroni-pico/blob/0bda2abd2acd4269725efb81ffd3f33621a8e176/drivers/trackball/trackball.cpp

Thanks for sharing :) I'm on my first i2c and stm project so I'll see if I can figure this out :) Cheers for that! @Gadgetoid

ShiftedMr commented 3 years ago

Making some progress; One thing I'm not sure of (I've been able to get the light working) The example code doesn't mention but can you read the current value of the LED register? or do I need the masterI2C device to just keep track?

Thanks! -F

ShiftedMr commented 3 years ago

Any of you use pullup resistors with this? Getting intermittent failures on i2c; trying to determine if it's my wiring. I'm almost ready to share what I've got with a bit more tweaking

wez commented 3 years ago

I added pullups on my i2c bus; I can't remember what resistor value I ended up using and I'm too lazy to open up the case to look it up; sorry :-)

ShiftedMr commented 3 years ago

I added pullups on my i2c bus; I can't remember what resistor value I ended up using and I'm too lazy to open up the case to look it up; sorry :-)

No worries! thanks for verification; which controller board did you end up using? and is your code open?

wez commented 3 years ago

I played with an atsamd21 but am using an nrf52840. The internal pullup strength of those two are different, which is one of the reasons that I ended up added external pullups!

The overall code for my project isn't currently pushed anywhere, but the trackball bits are fairly self-contained:

#pragma once
#include "Manuform.h"

class PimoroniTrackball {
public:
  enum Reg {
    LedRed = 0,
    LedGreen = 1,
    LedBlue = 2,
    LedWhite = 3,
    Left = 4,
    Right = 5,
    Up = 6,
    Down = 7,
    Switch = 8,
    UserFlash = 0xd0,
    FlashPage = 0xf0,
    Int = 0xf9,
    ChipIdLow = 0xfa,
    ChipIdHigh = 0xfb,
    Version = 0xfc,
    I2CAddr = 0xfd,
    Ctrl = 0xfe,
  };

  enum CtrlMask {
    Sleep = 1,
    Reset = 2,
    FRead = 4,
    FWrite = 8,
  };

  enum IntMask {
    Triggered = 1,
    OutputEnable = 2,
  };

  enum SwitchStateMask {
    Pressed = 0x80,
  };

  enum MouseMode {
    ScrollWheel,
    Pointer,
  };

  static const constexpr uint16_t kChipId = 0xba11;
  static const constexpr uint8_t kAddress = 0x0a;
  static const constexpr int kInterruptPin =
#ifdef ARDUINO_PARTICLE_XENON
      PIN_D10
#else
      PIN_SERIAL1_RX
#endif
      ;

  static const constexpr bool kUseInterrupt = true;

  void begin() {
    setRegister(Reg::Ctrl, CtrlMask::Reset);
    delay(10);
    pinMode(kInterruptPin, INPUT);
    digitalWrite(kInterruptPin, LOW); // Disable pull up

    if (kUseInterrupt) {
      attachInterrupt(digitalPinToInterrupt(kInterruptPin),
                      PimoroniTrackball::serviceInterrupt,
                      FALLING);
    }

    auto mask = getRegister(Reg::Int);
    setRegister(Reg::Int, mask | IntMask::OutputEnable);
  }

  uint8_t getRegister(Reg reg) {
    Wire.beginTransmission(kAddress);
    Wire.write(uint8_t(reg));
    Wire.endTransmission();
    return Wire.read();
  }

  void setRegister(Reg reg, uint8_t value) {
    Wire.beginTransmission(kAddress);
    uint8_t data[2] = {reg, value};
    Wire.write(data, sizeof(data));
    Wire.endTransmission();
  }

  // https://github.com/pimoroni/trackball-python/issues/5#issuecomment-607951295
  struct State {
    uint8_t left;
    uint8_t right;
    uint8_t up;
    uint8_t down;
    // The number of presses since the last read.
    // SwitchStateMask::Pressed indicates whether it is currently down
    uint8_t button;
    // Corresponds to SwitchStateMask::Pressed from the button field
    bool pressed;

    bool isEmpty() {
      return left == 0 && right == 0 && up == 0 && down == 0 && !pressed;
    }

    int8_t x() {
      return int16_t(right) - int16_t(left);
    }
    int8_t y() {
      return int16_t(down) - int16_t(up);
    }

    void clear() {
      left = 0;
      right = 0;
      up = 0;
      down = 0;
      button = 0;
      pressed = false;
    }
  };

  State read() {
    Wire.beginTransmission(kAddress);
    Wire.write(uint8_t(Reg::Left));
    Wire.endTransmission();

    State state;
    Wire.requestFrom(kAddress, 5, true);
    state.left = Wire.read();
    state.right = Wire.read();
    state.up = Wire.read();
    state.down = Wire.read();
    state.button = Wire.read();
    state.pressed = (state.button & SwitchStateMask::Pressed) != 0;
    state.button &= ~SwitchStateMask::Pressed;

    return state;
  }

  static void serviceInterrupt();

  bool wasInterrupted() {
    if (kUseInterrupt) {
      return interupted;
    }
    return digitalRead(kInterruptPin) == LOW;
  }

  void process() {
    if (wasInterrupted()) {
      updateState();
      applyToMouse();
    } else if (!lastState.isEmpty()) {
      MouseReport empty;
      ConnectedHost::active->mouseReport(empty);
      lastState.clear();
    }
  }

  void updateState() {
    interupted = false;
    lastState = read();
  }

  void applyToMouse() {
    if (lastState.isEmpty()) {
      return;
    }
    cycle();

    MouseReport report;
    report.buttons = lastState.pressed ? 1 : 0;
    switch (mode) {
    case MouseMode::ScrollWheel:
      report.wheel = -lastState.y();
      break;
    case MouseMode::Pointer:
      report.x = accelerated(lastState.x());
      report.y = accelerated(lastState.y());
      break;
    }
    ConnectedHost::active->mouseReport(report);
  }

  static int8_t pow(int8_t v, uint8_t n) {
    int32_t b = int32_t(v);
    int32_t scaled = 3;
    bool preservedSign = n & 1;

    while (n--) {
      scaled *= b;
    }

    if (!preservedSign) {
      if (v < 0) {
        scaled = -scaled;
      }
    }

    if (scaled > 127) {
      return 127;
    }
    if (scaled < -127) {
      return -127;
    }
    return int8_t(scaled);
  }

  static int8_t accelerated(int8_t v) {
    return pow(v, 3);
  }

  void cycle() {
    uint8_t rgbw[5] = {Reg::LedRed, 0, 0, 0, 0};
    switch (++ledState) {
    case 1:
      rgbw[2] = 0x7f;
      break;
    case 2:
      rgbw[1] = 0x7f;
      break;
    case 3:
      rgbw[3] = 0x7f;
      ledState = 0;
      break;
    }
    Wire.beginTransmission(kAddress);
    Wire.write(rgbw, sizeof(rgbw));
    Wire.endTransmission();
  }

  volatile bool interupted{false};
  State lastState;
  volatile int ledState{0};
  MouseMode mode{MouseMode::ScrollWheel};
};

PimoroniTrackball Trackball;

void PimoroniTrackball::serviceInterrupt() {
  Trackball.interupted = true;
}