From edd76fa1db7e1c2fe0101ca15260ebf7cd823c86 Mon Sep 17 00:00:00 2001
From: Slavey Karadzhov <slav@attachix.com>
Date: Mon, 2 May 2022 11:46:40 +0200
Subject: [PATCH] Added Switch Joycon sample.

---
 Sming/Libraries/SwitchJoycon/.cs              |   0
 Sming/Libraries/SwitchJoycon/README.rst       |  76 +++
 Sming/Libraries/SwitchJoycon/component.mk     |   5 +
 .../SwitchJoycon/samples/Bluetooth_Joycon/.cs |   0
 .../samples/Bluetooth_Joycon/Makefile         |   9 +
 .../samples/Bluetooth_Joycon/README.rst       |  23 +
 .../Bluetooth_Joycon/app/application.cpp      |  54 +++
 .../samples/Bluetooth_Joycon/component.mk     |   1 +
 Sming/Libraries/SwitchJoycon/src/.cs          |   0
 .../SwitchJoycon/src/SwitchJoycon.cpp         | 438 ++++++++++++++++++
 .../Libraries/SwitchJoycon/src/SwitchJoycon.h | 154 ++++++
 .../src/SwitchJoyconConnection.cpp            |  17 +
 .../SwitchJoycon/src/SwitchJoyconConnection.h |  36 ++
 13 files changed, 813 insertions(+)
 create mode 100644 Sming/Libraries/SwitchJoycon/.cs
 create mode 100644 Sming/Libraries/SwitchJoycon/README.rst
 create mode 100644 Sming/Libraries/SwitchJoycon/component.mk
 create mode 100644 Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/.cs
 create mode 100644 Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/Makefile
 create mode 100644 Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/README.rst
 create mode 100644 Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/app/application.cpp
 create mode 100644 Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/component.mk
 create mode 100644 Sming/Libraries/SwitchJoycon/src/.cs
 create mode 100644 Sming/Libraries/SwitchJoycon/src/SwitchJoycon.cpp
 create mode 100644 Sming/Libraries/SwitchJoycon/src/SwitchJoycon.h
 create mode 100644 Sming/Libraries/SwitchJoycon/src/SwitchJoyconConnection.cpp
 create mode 100644 Sming/Libraries/SwitchJoycon/src/SwitchJoyconConnection.h

diff --git a/Sming/Libraries/SwitchJoycon/.cs b/Sming/Libraries/SwitchJoycon/.cs
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Sming/Libraries/SwitchJoycon/README.rst b/Sming/Libraries/SwitchJoycon/README.rst
new file mode 100644
index 0000000000..537b2c9a4a
--- /dev/null
+++ b/Sming/Libraries/SwitchJoycon/README.rst
@@ -0,0 +1,76 @@
+Switch Joycon
+=============
+
+.. highlight:: c++
+
+Introduction
+------------
+This library allows you to make the ESP32 act as a Nintendo Switch Joycon and control what it does.  
+The library uses :library:`NimBLE` for faster and lighter communication.
+
+Disclaimer
+----------
+We are not affiliated, associated, authorized, endorsed by, or in any way officially connected with Nintendo, 
+or any of its subsidiaries or its affiliates. 
+The names Nintendo, Nintendo Switch and Joycon as well as related names, marks, emblems and images are 
+registered trademarks of their respective owners.
+
+Features
+--------
+
+Using this library you can do the following:
+
+ - Button press and release (16 buttons)
+ - Switch Hat (1 hat )
+ - Rotate 4 Axis
+
+Using
+-----
+
+1. Add ``COMPONENT_DEPENDS += SwitchJoycon`` to your application componenent.mk file.
+2. Add these lines to your application::
+
+	#include <SwitchJoycon.h>
+	
+	namespace
+	{
+		SwitchJoycon joycon;
+	
+		// ...
+	
+	} // namespace
+		
+	void init()
+	{
+		// ...
+		
+		joycon.begin();
+	}
+
+
+Notes
+-----
+By default, reports are sent on every button press/release or axis/hat movement, however this can be disabled::
+
+	joycon.setAutoReport(false);
+ 
+and then you should manually call sendReport on the joycon instance as shown below::
+
+	joycon.sendReport();
+
+
+HID Debugging
+-------------
+
+On Linux you can install `hid-tools <https://gitlab.freedesktop.org/libevdev/hid-tools>`__ using the command below::
+
+	sudo pip3 install .
+
+Once installed hid-recorder can be used to check the device HID report description and sniff the different reports::
+
+	sudo hid-recorder
+
+Useful Links
+------------
+- `Tutorial about USB HID Report Descriptors <https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/>`__
+- `HID constants <https://github.com/katyo/hid_def/blob/master/include/hid_def.h>`__
diff --git a/Sming/Libraries/SwitchJoycon/component.mk b/Sming/Libraries/SwitchJoycon/component.mk
new file mode 100644
index 0000000000..7d86756dd3
--- /dev/null
+++ b/Sming/Libraries/SwitchJoycon/component.mk
@@ -0,0 +1,5 @@
+COMPONENT_SOC := esp32*
+COMPONENT_DEPENDS := NimBLE
+
+COMPONENT_SRCDIRS := src
+COMPONENT_INCDIRS := $(COMPONENT_SRCDIRS)
\ No newline at end of file
diff --git a/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/.cs b/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/.cs
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/Makefile b/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/Makefile
new file mode 100644
index 0000000000..ff51b6c3a7
--- /dev/null
+++ b/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/Makefile
@@ -0,0 +1,9 @@
+#####################################################################
+#### Please don't change this file. Use component.mk instead ####
+#####################################################################
+
+ifndef SMING_HOME
+$(error SMING_HOME is not set: please configure it as an environment variable)
+endif
+
+include $(SMING_HOME)/project.mk
diff --git a/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/README.rst b/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/README.rst
new file mode 100644
index 0000000000..94e0171a65
--- /dev/null
+++ b/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/README.rst
@@ -0,0 +1,23 @@
+Switch Joycon
+=============
+
+Introduction
+------------
+This sample turns the ESP32 into a Switch Joycon (Bluetooth LE gamepad) that presses buttons and moves axis
+
+Possible buttons are 0 through to 15.
+
+Possible HAT switch position values are:
+Centered, Up, UpRight, Right, DownRight, Down, DownLeft, Left, UpLeft.
+
+
+Testing
+-------
+
+You can use one of the following applications on your PC to test and see all buttons that were clicked.
+
+On Linux install ``jstest-gtk`` to test the ESP32 gamepad. Under Ubuntu this can be done by typing the following command::
+
+	sudo apt install jstest-gtk
+	
+On Windows use this `Windows test application <http://www.planetpointy.co.uk/joystick-test-application/>`__.
\ No newline at end of file
diff --git a/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/app/application.cpp b/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/app/application.cpp
new file mode 100644
index 0000000000..caa9ef8fe4
--- /dev/null
+++ b/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/app/application.cpp
@@ -0,0 +1,54 @@
+#include <SmingCore.h>
+#include <SwitchJoycon.h>
+
+namespace
+{
+Timer procTimer;
+
+void onConnect(NimBLEServer& server);
+void onDisconnect(NimBLEServer& server);
+
+SwitchJoycon joycon(SwitchJoycon::Type::Left, 100, onConnect, onDisconnect);
+
+void loop()
+{
+	if(!joycon.isConnected()) {
+		return;
+	}
+
+	uint8_t button = random(0, 15);
+
+	joycon.press(button);
+	joycon.setHat(SwitchJoycon::JoystickPosition::UpLeft);
+	delay(5000);
+
+	joycon.release(button);
+	joycon.setHat(SwitchJoycon::JoystickPosition::Center);
+	delay(5000);
+}
+
+void onConnect(NimBLEServer& server)
+{
+	Serial.println("Connected :) !");
+
+	procTimer.initializeMs(500, loop).start();
+}
+
+void onDisconnect(NimBLEServer& server)
+{
+	procTimer.stop();
+	Serial.println("Disconnected :(!");
+}
+
+} // namespace
+
+void init()
+{
+	Serial.begin(COM_SPEED_SERIAL);
+	Serial.systemDebugOutput(true);
+
+	Serial.println("Starting Joycon Gamepad sample!");
+	joycon.begin();
+	// Auto reporting is enabled by default.
+	// Use joycon.setAutoReport(false); to disable auto reporting, and then use joycon.sendReport(); as needed
+}
diff --git a/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/component.mk b/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/component.mk
new file mode 100644
index 0000000000..1b24857470
--- /dev/null
+++ b/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/component.mk
@@ -0,0 +1 @@
+COMPONENT_DEPENDS := SwitchJoycon
diff --git a/Sming/Libraries/SwitchJoycon/src/.cs b/Sming/Libraries/SwitchJoycon/src/.cs
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Sming/Libraries/SwitchJoycon/src/SwitchJoycon.cpp b/Sming/Libraries/SwitchJoycon/src/SwitchJoycon.cpp
new file mode 100644
index 0000000000..75f10086e9
--- /dev/null
+++ b/Sming/Libraries/SwitchJoycon/src/SwitchJoycon.cpp
@@ -0,0 +1,438 @@
+#include "SwitchJoycon.h"
+
+#include <WString.h>
+#include <NimBLEDevice.h>
+#include <NimBLEUtils.h>
+#include <NimBLEServer.h>
+#include <HIDTypes.h>
+#include <debug_progmem.h>
+
+#if DEBUG_VERBOSE_LEVEL == 3
+#include <Services/HexDump/HexDump.h>
+HexDump dump;
+#endif
+
+// TODO: Move report description to progmem
+uint8_t tempHidReportDescriptor[150];
+int hidReportDescriptorSize = 0;
+
+constexpr uint8_t GAMEPAD_DEFAULT_REPORT_ID = 63;
+constexpr uint8_t JOYSTICK_TYPE_GAMEPAD = 0x05;
+
+void SwitchJoycon::resetButtons()
+{
+	memset(&state.buttons, 0, sizeof(state.buttons));
+}
+
+bool SwitchJoycon::begin()
+{
+	if(started) {
+		debug_w("Service already started");
+		return false;
+	}
+
+	uint8_t axisCount = 0;
+	buttonCount = 0;
+	hatSwitchCount = 0;
+	switch(controllerType) {
+	case Type::Left:
+		/* fall through */
+	case Type::Right:
+		buttonCount = 16; // buttonCount;
+		hatSwitchCount = 1;
+		axisCount = 4; // x, y, z and z rotation
+		break;
+	case Type::ProController:
+		buttonCount = 16; // buttonCount;
+		hatSwitchCount = 1;
+		axisCount = 4; // x, y, z and z rotation
+		break;
+	}
+
+	started = true;
+	state.hat = static_cast<uint8_t>(JoystickPosition::Center);
+	state.leftX[0] = 0x00;
+	state.leftX[1] = 0x80;
+	state.leftY[0] = 0x00;
+	state.leftY[1] = 0x80;
+	state.rightX[0] = 0x00;
+	state.rightX[1] = 0x80;
+	state.rightY[0] = 0x00;
+	state.rightY[1] = 0x80;
+
+	/**
+	 * For HID debugging see: https://gitlab.freedesktop.org/libevdev/hid-tools
+	 * For HID report descriptors see: https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/
+	 */
+
+	hidReportDescriptorSize = 0;
+
+	// USAGE_PAGE (Generic Desktop)
+	tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE_PAGE(1);
+	tempHidReportDescriptor[hidReportDescriptorSize++] = 0x01;
+
+	// USAGE (Joystick - 0x04; Gamepad - 0x05; Multi-axis Controller - 0x08)
+	tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE(1);
+	tempHidReportDescriptor[hidReportDescriptorSize++] = JOYSTICK_TYPE_GAMEPAD;
+
+	// COLLECTION (Application)
+	tempHidReportDescriptor[hidReportDescriptorSize++] = COLLECTION(1);
+	tempHidReportDescriptor[hidReportDescriptorSize++] = 0x01;
+
+	// REPORT_ID (Default: 3)
+	tempHidReportDescriptor[hidReportDescriptorSize++] = REPORT_ID(1);
+	tempHidReportDescriptor[hidReportDescriptorSize++] = GAMEPAD_DEFAULT_REPORT_ID;
+
+	if(buttonCount > 0) {
+		// USAGE_PAGE (Button)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE_PAGE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x09;
+
+		// USAGE_MINIMUM (Button 1)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE_MINIMUM(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x01;
+
+		// USAGE_MAXIMUM (Button 16)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE_MAXIMUM(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = buttonCount;
+
+		// LOGICAL_MINIMUM (0)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = LOGICAL_MINIMUM(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x00;
+
+		// LOGICAL_MAXIMUM (1)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = LOGICAL_MAXIMUM(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x01;
+
+		// REPORT_SIZE (1)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = REPORT_SIZE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x01;
+
+		// REPORT_COUNT (# of buttons)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = REPORT_COUNT(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = buttonCount;
+
+		// INPUT (Data,Var,Abs)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = HIDINPUT(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x02;
+	} // buttonCount
+
+	if(hatSwitchCount > 0) {
+		// USAGE_PAGE (Generic Desktop)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE_PAGE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x01;
+
+		// USAGE (Hat Switch)
+		for(int i = 0; i < hatSwitchCount; i++) {
+			tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE(1);
+			tempHidReportDescriptor[hidReportDescriptorSize++] = 0x39;
+		}
+
+		// Logical Min (0)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = LOGICAL_MINIMUM(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x00;
+
+		// Logical Max (7)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = LOGICAL_MAXIMUM(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x07;
+
+		// Report Size (4)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = REPORT_SIZE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x04;
+
+		// Report Count (1)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = REPORT_COUNT(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = hatSwitchCount;
+
+		// Input (Data, Variable, Absolute)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = HIDINPUT(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x42;
+
+		// -- Padding for the 4 unused bits in the hat switch byte --
+		tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE_PAGE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x09;
+
+		// Report Size (4)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = REPORT_SIZE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x04;
+
+		// Report Count (1)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = REPORT_COUNT(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x01;
+
+		// Input (Cnst,Arr,Abs)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = HIDINPUT(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x01;
+	}
+
+	if(axisCount > 0) {
+		// USAGE_PAGE (Generic Desktop)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE_PAGE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x01;
+
+		// USAGE (X)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x30;
+
+		// USAGE (Y)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x31;
+
+		// USAGE (Rx)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x33;
+
+		// USAGE (Ry)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = USAGE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x34;
+
+		// LOGICAL_MINIMUM(255)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = LOGICAL_MINIMUM(2);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x00;
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x00;
+
+		// LOGICAL_MAXIMUM (255)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = LOGICAL_MAXIMUM(3);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0xFF;
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0xFF;
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x00;
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x00;
+
+		// REPORT_SIZE (16)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = REPORT_SIZE(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x10;
+
+		// REPORT_COUNT (axisCount)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = REPORT_COUNT(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = axisCount;
+
+		// INPUT (Data,Var,Abs)
+		tempHidReportDescriptor[hidReportDescriptorSize++] = HIDINPUT(1);
+		tempHidReportDescriptor[hidReportDescriptorSize++] = 0x02;
+	}
+
+	// END_COLLECTION
+	tempHidReportDescriptor[hidReportDescriptorSize++] = END_COLLECTION(0);
+
+	xTaskCreate(startServer, "server", 20000, (void*)this, 5, &taskHandle);
+
+	return true;
+}
+
+void SwitchJoycon::end()
+{
+	if(!started) {
+		return;
+	}
+
+	if(taskHandle != nullptr) {
+		vTaskDelete(taskHandle);
+	}
+
+	NimBLEDevice::deinit(true);
+	delete hid;
+	hid = nullptr;
+	delete inputGamepad;
+	inputGamepad = nullptr;
+	connectionStatus = nullptr;
+	memset(&state, 0, sizeof(state));
+
+	started = false;
+}
+
+void SwitchJoycon::sendReport(void)
+{
+	if(!isConnected()) {
+		return;
+	}
+
+	debug_d("Sending report ....");
+
+#if DEBUG_VERBOSE_LEVEL == 3
+	dump.resetAddr();
+	dump.print(reinterpret_cast<uint8_t*>(&state), sizeof(state));
+#endif
+
+	debug_d("=================");
+
+	this->inputGamepad->setValue(state);
+	this->inputGamepad->notify();
+}
+
+bool SwitchJoycon::isPressed(uint8_t button)
+{
+	uint8_t index = button / 8;
+	uint8_t bit = button % 8;
+
+	return bitRead(state.buttons[index], bit);
+}
+void SwitchJoycon::press(uint8_t button)
+{
+	if(isPressed(button)) {
+		return;
+	}
+
+	uint8_t index = button / 8;
+	uint8_t bit = button % 8;
+
+	bitSet(state.buttons[index], bit);
+
+	if(autoReport) {
+		sendReport();
+	}
+}
+
+void SwitchJoycon::release(uint8_t button)
+{
+	if(!isPressed(button)) {
+		return;
+	}
+
+	uint8_t index = button / 8;
+	uint8_t bit = button % 8;
+
+	bitClear(state.buttons[index], bit);
+
+	if(autoReport) {
+		sendReport();
+	}
+}
+
+void SwitchJoycon::setHat(JoystickPosition position)
+{
+	if(state.hat == static_cast<uint8_t>(position)) {
+		return;
+	}
+
+	state.hat = static_cast<uint8_t>(position);
+
+	if(autoReport) {
+		sendReport();
+	}
+}
+
+void SwitchJoycon::setXAxis(int16_t value)
+{
+	// TODO: add value checks
+
+	state.leftX[0] = value;
+	state.leftX[1] = value >> 8;
+
+	if(autoReport) {
+		sendReport();
+	}
+}
+
+void SwitchJoycon::setYAxis(int16_t value)
+{
+	// TODO: add value checks
+
+	state.leftY[0] = value;
+	state.leftY[1] = value >> 8;
+
+	if(autoReport) {
+		sendReport();
+	}
+}
+
+void SwitchJoycon::setZAxis(int16_t value)
+{
+	// TODO: add value checks
+	//	if(value == -32768) {
+	//		value = -32767;
+	//	}
+
+	state.rightX[0] = value;
+	state.rightX[1] = value >> 8;
+
+	if(autoReport) {
+		sendReport();
+	}
+}
+
+void SwitchJoycon::setZAxisRotation(int16_t value)
+{
+	// TODO: add value checks
+
+	state.rightY[0] = value;
+	state.rightY[1] = value >> 8;
+
+	if(autoReport) {
+		sendReport();
+	}
+}
+
+bool SwitchJoycon::isConnected(void)
+{
+	if(connectionStatus == nullptr) {
+		return false;
+	}
+
+	return connectionStatus->connected;
+}
+
+void SwitchJoycon::setBatteryLevel(uint8_t level)
+{
+	batteryLevel = level;
+	if(hid != nullptr) {
+		hid->setBatteryLevel(batteryLevel);
+	}
+}
+
+void SwitchJoycon::startServer(void* arg)
+{
+	SwitchJoycon* joycon = static_cast<SwitchJoycon*>(arg);
+
+	String deviceName;
+	uint16_t productId = 0;
+	// See: http://gtoal.com/vectrex/vecx-colour/SDL/src/joystick/controller_type.h
+	switch(joycon->controllerType) {
+	case Type::Left:
+		deviceName = F("Joy-Con (L)");
+		productId = 0x2006;
+		break;
+	case Type::Right:
+		deviceName = F("Joy-Con (R)");
+		productId = 0x2007;
+		break;
+	case Type::ProController:
+		deviceName = F("Pro Controller");
+		productId = 0x2009;
+		break;
+	}
+
+	NimBLEDevice::init(deviceName.c_str());
+	NimBLEServer* server = NimBLEDevice::createServer();
+	server->setCallbacks(joycon->connectionStatus);
+
+	delete joycon->hid; // TODO: ?!
+	joycon->hid = new NimBLEHIDDevice(server);
+	joycon->inputGamepad = joycon->hid->inputReport(GAMEPAD_DEFAULT_REPORT_ID);
+	joycon->connectionStatus->inputGamepad = joycon->inputGamepad;
+
+	joycon->hid->manufacturer()->setValue("Nintendo");
+	joycon->hid->pnp(0x01, __builtin_bswap16(0x057e), __builtin_bswap16(productId), 0x0110);
+	joycon->hid->hidInfo(0x00, 0x01);
+
+	NimBLEDevice::setSecurityAuth(true, true, true);
+
+#if DEBUG_VERBOSE_LEVEL == 3
+	dump.resetAddr();
+	dump.print(tempHidReportDescriptor, hidReportDescriptorSize);
+#endif
+
+	joycon->hid->reportMap(tempHidReportDescriptor, hidReportDescriptorSize);
+	joycon->hid->startServices();
+
+	joycon->onStarted(server);
+
+	NimBLEAdvertising* pAdvertising = server->getAdvertising();
+	pAdvertising->setAppearance(HID_GAMEPAD);
+	pAdvertising->addServiceUUID(joycon->hid->hidService()->getUUID());
+	pAdvertising->start();
+	joycon->hid->setBatteryLevel(joycon->batteryLevel);
+
+	debug_d("Advertising started!");
+
+	vTaskDelay(portMAX_DELAY);
+}
diff --git a/Sming/Libraries/SwitchJoycon/src/SwitchJoycon.h b/Sming/Libraries/SwitchJoycon/src/SwitchJoycon.h
new file mode 100644
index 0000000000..86a2e63011
--- /dev/null
+++ b/Sming/Libraries/SwitchJoycon/src/SwitchJoycon.h
@@ -0,0 +1,154 @@
+#pragma once
+
+#include <Delegate.h>
+
+#include "sdkconfig.h"
+#if defined(CONFIG_BT_ENABLED)
+
+#include "nimconfig.h"
+#if defined(CONFIG_BT_NIMBLE_ROLE_PERIPHERAL)
+
+#include "SwitchJoyconConnection.h"
+#include <NimBLEHIDDevice.h>
+#include <NimBLECharacteristic.h>
+
+class SwitchJoycon
+{
+public:
+	enum class Type {
+		Left = 0,
+		Right,
+		ProController = 3,
+	};
+
+	enum class Button {
+		ButtonA = 0,
+		ButtonX,
+		ButtonB,
+		ButtonY,
+		ButtonSl,
+		ButtonSr,
+
+		ButtonMunus = 8,
+		ButtonPlus,
+
+		ButtonHome = 12,
+		ButtonCapture,
+		ButtonStickrl,
+		ButtonZrl,
+	};
+
+	enum class JoystickPosition {
+		Right = 0,
+		DownRight,
+		Down,
+		DownLeft,
+		Left,
+		UpLeft,
+		Up,
+		UpRight,
+		Center,
+	};
+
+	struct Gamepad // {00 00} 08 {00 80} {00 80} {00 80} {00 80}
+	{
+		uint8_t buttons[2];
+		uint8_t hat;
+		uint8_t leftX[2];
+		uint8_t leftY[2];
+		uint8_t rightX[2];
+		uint8_t rightY[2];
+	};
+
+	SwitchJoycon(Type type, uint8_t batteryLevel = 100, SwitchJoyconConnection::Callback onConnected = nullptr,
+				 SwitchJoyconConnection::Callback onDisconnected = nullptr)
+		: controllerType(type), batteryLevel(batteryLevel)
+	{
+		connectionStatus = new SwitchJoyconConnection(onConnected, onDisconnected);
+	}
+
+	virtual ~SwitchJoycon()
+	{
+		end();
+	}
+
+	bool begin();
+
+	void end();
+
+	void setType(Type type)
+	{
+		controllerType = type;
+	}
+
+	void setBatteryLevel(uint8_t level);
+
+	// Buttons
+
+	void press(Button button)
+	{
+		press(static_cast<uint8_t>(button));
+	}
+
+	void press(uint8_t button);
+
+	void release(Button button)
+	{
+		release(static_cast<uint8_t>(button));
+	}
+
+	void release(uint8_t button);
+
+	// Set Axis Values
+
+	void setXAxis(int16_t value);
+	void setYAxis(int16_t value);
+	void setZAxis(int16_t value);
+	void setZAxisRotation(int16_t value);
+
+	// Hat
+
+	void setHat(JoystickPosition position);
+
+	void setAutoReport(bool autoReport)
+	{
+		this->autoReport = autoReport;
+	}
+
+	void sendReport();
+	bool isPressed(uint8_t button);
+	bool isConnected();
+	void resetButtons();
+
+protected:
+	virtual void onStarted(NimBLEServer* pServer){};
+
+private:
+	bool started{false};
+	TaskHandle_t taskHandle{nullptr}; // owned
+
+	// Joystick Type
+	Type controllerType;
+
+	// Gamepad State
+	Gamepad state;
+	uint8_t batteryLevel{0};
+	uint8_t buttonCount{0};
+	uint8_t hatSwitchCount{0};
+
+	bool autoReport{true};
+	size_t reportSize{0};
+
+	// HID Settings
+	NimBLEHIDDevice* hid{nullptr}; // owned
+	uint8_t hidReportId{0};
+
+	// Connection status and gamepad
+	SwitchJoyconConnection* connectionStatus{nullptr};
+	NimBLECharacteristic* inputGamepad{nullptr}; // owned
+
+	static void startServer(void* pvParameter);
+};
+
+#endif // CONFIG_BT_NIMBLE_ROLE_PERIPHERAL
+#endif // CONFIG_BT_ENABLED
diff --git a/Sming/Libraries/SwitchJoycon/src/SwitchJoyconConnection.cpp b/Sming/Libraries/SwitchJoycon/src/SwitchJoyconConnection.cpp
new file mode 100644
index 0000000000..df2347e7d0
--- /dev/null
+++ b/Sming/Libraries/SwitchJoycon/src/SwitchJoyconConnection.cpp
@@ -0,0 +1,17 @@
+#include "SwitchJoyconConnection.h"
+
+void SwitchJoyconConnection::onConnect(NimBLEServer* server)
+{
+	connected = true;
+	if(connectCallback) {
+		connectCallback(*server);
+	}
+}
+
+void SwitchJoyconConnection::onDisconnect(NimBLEServer* server)
+{
+	connected = false;
+	if(connectCallback) {
+		disconnectCallback(*server);
+	}
+}
diff --git a/Sming/Libraries/SwitchJoycon/src/SwitchJoyconConnection.h b/Sming/Libraries/SwitchJoycon/src/SwitchJoyconConnection.h
new file mode 100644
index 0000000000..c96a29cead
--- /dev/null
+++ b/Sming/Libraries/SwitchJoycon/src/SwitchJoyconConnection.h
@@ -0,0 +1,36 @@
+#ifndef ESP32_BLE_CONNECTION_STATUS_H
+#define ESP32_BLE_CONNECTION_STATUS_H
+#include "sdkconfig.h"
+#if defined(CONFIG_BT_ENABLED)
+
+#include "nimconfig.h"
+#if defined(CONFIG_BT_NIMBLE_ROLE_PERIPHERAL)
+
+#include <NimBLEServer.h>
+#include <NimBLECharacteristic.h>
+
+class SwitchJoyconConnection : public NimBLEServerCallbacks
+{
+public:
+	using Callback = Delegate<void(NimBLEServer& server)>;
+
+	bool connected{false};
+	NimBLECharacteristic* inputGamepad{nullptr};
+
+	SwitchJoyconConnection(Callback onConnected = nullptr, Callback onDisconnected = nullptr)
+	{
+		connectCallback = onConnected;
+		disconnectCallback = onDisconnected;
+	}
+
+	void onConnect(NimBLEServer* pServer);
+	void onDisconnect(NimBLEServer* pServer);
+
+private:
+	Callback connectCallback;
+	Callback disconnectCallback;
+};
+
+#endif // CONFIG_BT_NIMBLE_ROLE_PERIPHERAL
+#endif // CONFIG_BT_ENABLED
+#endif // ESP32_BLE_CONNECTION_STATUS_H