Implement controller LED and battery state extensions

This commit is contained in:
Cameron Gutman 2023-07-02 19:03:31 -05:00
parent 803ad116fb
commit d4079940b4
8 changed files with 196 additions and 7 deletions

View File

@ -2257,6 +2257,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
controllerHandler.handleSetMotionEventState(controllerNumber, motionType, reportRateHz);
}
@Override
public void setControllerLED(short controllerNumber, byte r, byte g, byte b) {
controllerHandler.handleSetControllerLED(controllerNumber, r, g, b);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (!surfaceCreated) {

View File

@ -3,11 +3,16 @@ package com.limelight.binding.input;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.hardware.BatteryState;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.input.InputManager;
import android.hardware.lights.Light;
import android.hardware.lights.LightState;
import android.hardware.lights.LightsManager;
import android.hardware.lights.LightsRequest;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.media.AudioAttributes;
@ -56,6 +61,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private static final short MAX_GAMEPADS = 16; // Limited by bits in activeGamepadMask
private static final int BATTERY_RECHECK_INTERVAL_MS = 120 * 1000;
private static final Map<Integer, Integer> ANDROID_TO_LI_BUTTON_MAP = Map.ofEntries(
Map.entry(KeyEvent.KEYCODE_BUTTON_A, ControllerPacket.A_FLAG),
Map.entry(KeyEvent.KEYCODE_BUTTON_B, ControllerPacket.B_FLAG),
@ -867,6 +874,68 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
private static boolean areBatteryCapacitiesEqual(float first, float second) {
// With no NaNs involved, it is a simple equality comparison.
if (!Float.isNaN(first) && !Float.isNaN(second)) {
return first == second;
}
else {
// If we have a NaN in one or both positions, compare NaN-ness instead.
// Equality comparisons will always return false for NaN.
return Float.isNaN(first) == Float.isNaN(second);
}
}
private void sendControllerBatteryPacket(InputDeviceContext context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
int currentBatteryStatus = context.inputDevice.getBatteryState().getStatus();
float currentBatteryCapacity = context.inputDevice.getBatteryState().getCapacity();
if (currentBatteryStatus != context.lastReportedBatteryStatus ||
!areBatteryCapacitiesEqual(currentBatteryCapacity, context.lastReportedBatteryCapacity)) {
byte state;
byte percentage;
switch (currentBatteryStatus) {
case BatteryState.STATUS_UNKNOWN:
state = MoonBridge.LI_BATTERY_STATE_UNKNOWN;
break;
case BatteryState.STATUS_CHARGING:
state = MoonBridge.LI_BATTERY_STATE_CHARGING;
break;
case BatteryState.STATUS_DISCHARGING:
state = MoonBridge.LI_BATTERY_STATE_DISCHARGING;
break;
case BatteryState.STATUS_NOT_CHARGING:
state = MoonBridge.LI_BATTERY_STATE_NOT_CHARGING;
break;
case BatteryState.STATUS_FULL:
state = MoonBridge.LI_BATTERY_STATE_FULL;
break;
default:
return;
}
if (Float.isNaN(currentBatteryCapacity)) {
percentage = MoonBridge.LI_BATTERY_PERCENTAGE_UNKNOWN;
}
else {
percentage = (byte)(currentBatteryCapacity * 100);
}
conn.sendControllerBatteryEvent((byte)context.controllerNumber, state, percentage);
context.lastReportedBatteryStatus = currentBatteryStatus;
context.lastReportedBatteryCapacity = currentBatteryCapacity;
}
}
}
private void sendControllerInputPacket(GenericControllerContext originalContext) {
assignControllerNumberIfNeeded(originalContext);
@ -1685,13 +1754,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
deviceContext.leftTriggerMotor = leftTrigger;
deviceContext.rightTriggerMotor = rightTrigger;
if (deviceContext.controllerNumber == controllerNumber) {
deviceContext.leftTriggerMotor = leftTrigger;
deviceContext.rightTriggerMotor = rightTrigger;
if (deviceContext.controllerNumber == controllerNumber && deviceContext.quadVibrators) {
rumbleQuadVibrators(deviceContext.vibratorManager,
deviceContext.lowFreqMotor, deviceContext.highFreqMotor,
deviceContext.leftTriggerMotor, deviceContext.rightTriggerMotor);
if (deviceContext.quadVibrators) {
rumbleQuadVibrators(deviceContext.vibratorManager,
deviceContext.lowFreqMotor, deviceContext.highFreqMotor,
deviceContext.leftTriggerMotor, deviceContext.rightTriggerMotor);
}
}
}
}
@ -1791,6 +1862,36 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
public void handleSetControllerLED(short controllerNumber, byte r, byte g, byte b) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
if (deviceContext.controllerNumber == controllerNumber) {
// Create a new light session if one doesn't already exist
if (deviceContext.lightsSession == null) {
deviceContext.lightsSession = deviceContext.inputDevice.getLightsManager().openSession();
}
// Convert the RGB components into the integer value that LightState uses
int argbValue = 0xFF000000 | ((r << 16) & 0xFF0000) | ((g << 8) & 0xFF00) | (b & 0xFF);
LightState lightState = new LightState.Builder().setColor(argbValue).build();
// Set the RGB value for each RGB-controllable LED on the device
LightsRequest.Builder lightsRequestBuilder = new LightsRequest.Builder();
for (Light light : deviceContext.inputDevice.getLightsManager().getLights()) {
if (light.hasRgbControl()) {
lightsRequestBuilder.addLight(light, lightState);
}
}
// Apply the LED changes
deviceContext.lightsSession.requestLights(lightsRequestBuilder.build());
}
}
}
}
public boolean handleButtonUp(KeyEvent event) {
InputDeviceContext context = getContextForEvent(event);
if (context == null) {
@ -2362,6 +2463,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public InputDevice inputDevice;
public LightsManager.LightsSession lightsSession;
// These are BatteryState values, not Moonlight values
public int lastReportedBatteryStatus;
public float lastReportedBatteryCapacity;
public int leftStickXAxis = -1;
public int leftStickYAxis = -1;
@ -2406,6 +2513,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public long startDownTime = 0;
public final Runnable batteryStateUpdateRunnable = new Runnable() {
@Override
public void run() {
sendControllerBatteryPacket(InputDeviceContext.this);
// Requeue the callback
handler.postDelayed(this, BATTERY_RECHECK_INTERVAL_MS);
}
};
@Override
public void destroy() {
super.destroy();
@ -2424,7 +2541,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if (accelListener != null) {
inputDevice.getSensorManager().unregisterListener(accelListener);
}
if (lightsSession != null) {
lightsSession.close();
}
}
handler.removeCallbacks(batteryStateUpdateRunnable);
}
@Override
@ -2484,6 +2607,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if (inputDevice.getSensorManager().getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null) {
capabilities |= MoonBridge.LI_CCAP_GYRO;
}
if (inputDevice.getBatteryState().isPresent()) {
capabilities |= MoonBridge.LI_CCAP_BATTERY_STATE;
}
for (Light light : inputDevice.getLightsManager().getLights()) {
if (light.hasRgbControl()) {
capabilities |= MoonBridge.LI_CCAP_RGB_LED;
}
}
}
if (inputDevice.getVibrator().hasVibrator()) {
@ -2499,6 +2632,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
conn.sendControllerArrivalEvent((byte)controllerNumber, getActiveControllerMask(),
type, supportedButtonFlags, capabilities);
// After reporting arrival to the host, send initial battery state and begin monitoring
sendControllerBatteryPacket(this);
handler.postDelayed(batteryStateUpdateRunnable, BATTERY_RECHECK_INTERVAL_MS);
}
}

View File

@ -571,6 +571,10 @@ public class NvConnection {
}
}
public void sendControllerBatteryEvent(byte controllerNumber, byte batteryState, byte batteryPercentage) {
MoonBridge.sendControllerBatteryEvent(controllerNumber, batteryState, batteryPercentage);
}
public void sendUtf8Text(final String text) {
if (!isMonkey) {
MoonBridge.sendUtf8Text(text);

View File

@ -18,4 +18,6 @@ public interface NvConnectionListener {
void setHdrMode(boolean enabled, byte[] hdrMetadata);
void setMotionEventState(short controllerNumber, byte motionType, short reportRateHz);
void setControllerLED(short controllerNumber, byte r, byte g, byte b);
}

View File

@ -107,10 +107,21 @@ public class MoonBridge {
public static final short LI_CCAP_TOUCHPAD = 0x08;
public static final short LI_CCAP_ACCEL = 0x10;
public static final short LI_CCAP_GYRO = 0x20;
public static final short LI_CCAP_BATTERY_STATE = 0x40;
public static final short LI_CCAP_RGB_LED = 0x80;
public static final byte LI_MOTION_TYPE_ACCEL = 0x01;
public static final byte LI_MOTION_TYPE_GYRO = 0x02;
public static final byte LI_BATTERY_STATE_UNKNOWN = 0x00;
public static final byte LI_BATTERY_STATE_NOT_PRESENT = 0x01;
public static final byte LI_BATTERY_STATE_DISCHARGING = 0x02;
public static final byte LI_BATTERY_STATE_CHARGING = 0x03;
public static final byte LI_BATTERY_STATE_NOT_CHARGING = 0x04; // Connected to power but not charging
public static final byte LI_BATTERY_STATE_FULL = 0x05;
public static final byte LI_BATTERY_PERCENTAGE_UNKNOWN = (byte)0xFF;
private static AudioRenderer audioRenderer;
private static VideoDecoderRenderer videoRenderer;
private static NvConnectionListener connectionListener;
@ -307,6 +318,12 @@ public class MoonBridge {
}
}
public static void bridgeClSetControllerLED(short controllerNumber, byte r, byte g, byte b) {
if (connectionListener != null) {
connectionListener.setControllerLED(controllerNumber, r, g, b);
}
}
public static void setupBridge(VideoDecoderRenderer videoRenderer, AudioRenderer audioRenderer, NvConnectionListener connectionListener) {
MoonBridge.videoRenderer = videoRenderer;
MoonBridge.audioRenderer = audioRenderer;
@ -361,6 +378,8 @@ public class MoonBridge {
public static native int sendControllerMotionEvent(byte controllerNumber, byte motionType, float x, float y, float z);
public static native int sendControllerBatteryEvent(byte controllerNumber, byte batteryState, byte batteryPercentage);
public static native void sendKeyboardInput(short keyMap, byte keyDirection, byte modifier, byte flags);
public static native void sendMouseHighResScroll(short scrollAmount);

View File

@ -35,6 +35,7 @@ static jmethodID BridgeClConnectionStatusUpdateMethod;
static jmethodID BridgeClSetHdrModeMethod;
static jmethodID BridgeClRumbleTriggersMethod;
static jmethodID BridgeClSetMotionEventStateMethod;
static jmethodID BridgeClSetControllerLEDMethod;
static jbyteArray DecodedFrameBuffer;
static jshortArray DecodedAudioBuffer;
@ -98,6 +99,7 @@ Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) {
BridgeClSetHdrModeMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClSetHdrMode", "(Z[B)V");
BridgeClRumbleTriggersMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClRumbleTriggers", "(SSS)V");
BridgeClSetMotionEventStateMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClSetMotionEventState", "(SBS)V");
BridgeClSetControllerLEDMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClSetControllerLED", "(SBBB)V");
}
int BridgeDrSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) {
@ -374,6 +376,17 @@ void BridgeClSetMotionEventState(uint16_t controllerNumber, uint8_t motionType,
}
}
void BridgeClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) {
JNIEnv* env = GetThreadEnv();
// These jbyte casts are necessary to satisfy CheckJNI
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClSetControllerLEDMethod, controllerNumber, (jbyte)r, (jbyte)g, (jbyte)b);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
}
}
void BridgeClLogMessage(const char* format, ...) {
va_list va;
va_start(va, format);
@ -410,6 +423,7 @@ static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = {
.setHdrMode = BridgeClSetHdrMode,
.rumbleTriggers = BridgeClRumbleTriggers,
.setMotionEventState = BridgeClSetMotionEventState,
.setControllerLED = BridgeClSetControllerLED,
};
JNIEXPORT jint JNICALL

@ -1 +1 @@
Subproject commit c5dc45e1443363d95b9708de26e86ed57b6946e6
Subproject commit c0792168f5a7ed48fc6feeb7fce01b83df405df2

View File

@ -80,6 +80,14 @@ Java_com_limelight_nvstream_jni_MoonBridge_sendControllerMotionEvent(JNIEnv *env
return LiSendControllerMotionEvent(controllerNumber, motionType, x, y, z);
}
JNIEXPORT jint JNICALL
Java_com_limelight_nvstream_jni_MoonBridge_sendControllerBatteryEvent(JNIEnv *env, jclass clazz,
jbyte controllerNumber,
jbyte batteryState,
jbyte batteryPercentage) {
return LiSendControllerBatteryEvent(controllerNumber, batteryState, batteryPercentage);
}
JNIEXPORT void JNICALL
Java_com_limelight_nvstream_jni_MoonBridge_sendKeyboardInput(JNIEnv *env, jclass clazz, jshort keyCode, jbyte keyAction, jbyte modifiers, jbyte flags) {
LiSendKeyboardEvent2(keyCode, keyAction, modifiers, flags);