Compare commits
1 Commits
v8.4.5
...
semaphore-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aeaef7b788 |
@@ -5,12 +5,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to the [UHK Versioning](VERSIONING.md) conventions.
|
and this project adheres to the [UHK Versioning](VERSIONING.md) conventions.
|
||||||
|
|
||||||
## [8.4.5] - 2018-08-21
|
|
||||||
|
|
||||||
Device Protocol: 4.4.0 | Module Protocol: 4.0.0 | User Config: 4.1.0 | Hardware Config: 1.0.0
|
|
||||||
|
|
||||||
- Suppress pressed keys when the layer or keymap changes.
|
|
||||||
|
|
||||||
## [8.4.4] - 2018-08-14
|
## [8.4.4] - 2018-08-14
|
||||||
|
|
||||||
Device Protocol: 4.4.0 | Module Protocol: 4.0.0 | User Config: 4.1.0 | Hardware Config: 1.0.0
|
Device Protocol: 4.4.0 | Module Protocol: 4.0.0 | User Config: 4.1.0 | Hardware Config: 1.0.0
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -12,15 +12,19 @@ If you're one of the brave few who wants to hack the firmware then read on.
|
|||||||
|
|
||||||
`git clone --recursive git@github.com:UltimateHackingKeyboard/firmware.git`
|
`git clone --recursive git@github.com:UltimateHackingKeyboard/firmware.git`
|
||||||
|
|
||||||
2. Download and install MCUXpresso IDE for [Linux](https://storage.googleapis.com/ugl-static/mcuxpresso-ide/mcuxpressoide-10.2.1_795.x86_64.deb.bin), [Mac](https://storage.googleapis.com/ugl-static/mcuxpresso-ide/MCUXpressoIDE_10.2.1_795.pkg), or [Windows](https://storage.googleapis.com/ugl-static/mcuxpresso-ide/MCUXpressoIDE_10.2.1_795.exe).
|
2. Download and install MCUXpresso IDE for [Linux](https://storage.googleapis.com/ugl-static/mcuxpresso-ide/mcuxpressoide-10.1.1_606.x86_64.deb.bin), [Mac](https://storage.googleapis.com/ugl-static/mcuxpresso-ide/MCUXpressoIDE_10.1.1_606.pkg), or [Windows](https://storage.googleapis.com/ugl-static/mcuxpresso-ide/MCUXpressoIDE_10.1.1_606.exe).
|
||||||
|
|
||||||
3. In the IDE, import the project by invoking *File -> Import -> General -> Existing Projects into Workspace*, select the *left* or *right* directory depending on the desired firmware, then click on the *Finish* button.
|
3. In the IDE, import the project by invoking *File -> Import -> General -> Existing Projects into Workspace*, select the *left* or *right* directory depending on the desired firmware, then click on the *Finish* button.
|
||||||
|
|
||||||
4. In order to be able to flash the firmware via USB from the IDE, you must build [Agent](https://github.com/UltimateHackingKeyboard/agent) which is Git submodule of the this repo and located in the `lib/agent` directory.
|
## Building and flashing the firmware
|
||||||
|
|
||||||
5. Finally, in the IDE, click on *Run -> External Tools -> External Tools Configurations*, then select a release firmware to be flashed such as *uhk60-right_release_kboot*, and click on the *Run* button.
|
For the left keyboard half, make sure to power it via the right keyboard half (which must be powered via USB). Also connect the left keyboard half to your SEGGER J-Link USB debug probe (which must also be connected via USB). Then in KDS, click on *Run -> Run Configurations*, select *GDB SEGGER J-Link Debugging -> uhk60-left_release_jlink*, and click on the *Debug* button.
|
||||||
|
|
||||||
Going forward, it's easier to flash the firmware of your choice by using the downwards toolbar icon which is located rightwards of the *green play + toolbox icon*.
|
For the right keyboard half, flash [the bootloader](https://github.com/UltimateHackingKeyboard/bootloader) first.
|
||||||
|
|
||||||
|
At this point, you can flash the right firmware via USB from KDS. To achieve this, you must build [Agent](https://github.com/UltimateHackingKeyboard/agent) that is Git submodule of the this repo and located in the `lib/agent` directory. Then in KDS, click on *Run -> Run Configurations*, select *C/C++ Application -> uhk60-right_release_kboot*, and click on the *Run* button.
|
||||||
|
|
||||||
|
From this point on, you can upgrade the firmwares of both halves via USB by using the uhk60-left_release_kboot and uhk60-right_release_kboot run configurations. Alternatively, you can use your SEGGER J-Link probe.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
Submodule lib/agent updated: b41f14192a...80e8c014ec
@@ -5,7 +5,6 @@
|
|||||||
#include "config_parser/parse_keymap.h"
|
#include "config_parser/parse_keymap.h"
|
||||||
#include "config_parser/config_globals.h"
|
#include "config_parser/config_globals.h"
|
||||||
#include "macros.h"
|
#include "macros.h"
|
||||||
#include "usb_report_updater.h"
|
|
||||||
|
|
||||||
keymap_reference_t AllKeymaps[MAX_KEYMAP_NUM] = {
|
keymap_reference_t AllKeymaps[MAX_KEYMAP_NUM] = {
|
||||||
{
|
{
|
||||||
@@ -25,7 +24,6 @@ void SwitchKeymapById(uint8_t index)
|
|||||||
ValidatedUserConfigBuffer.offset = AllKeymaps[index].offset;
|
ValidatedUserConfigBuffer.offset = AllKeymaps[index].offset;
|
||||||
ParseKeymap(&ValidatedUserConfigBuffer, index, AllKeymapsCount, AllMacrosCount);
|
ParseKeymap(&ValidatedUserConfigBuffer, index, AllKeymapsCount, AllMacrosCount);
|
||||||
LedDisplay_UpdateText();
|
LedDisplay_UpdateText();
|
||||||
KeymapChanged = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SwitchKeymapByAbbreviation(uint8_t length, char *abbrev)
|
bool SwitchKeymapByAbbreviation(uint8_t length, char *abbrev)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ void updateLayerStates(void)
|
|||||||
for (uint8_t slotId=0; slotId<SLOT_COUNT; slotId++) {
|
for (uint8_t slotId=0; slotId<SLOT_COUNT; slotId++) {
|
||||||
for (uint8_t keyId=0; keyId<MAX_KEY_COUNT_PER_MODULE; keyId++) {
|
for (uint8_t keyId=0; keyId<MAX_KEY_COUNT_PER_MODULE; keyId++) {
|
||||||
key_state_t *keyState = &KeyStates[slotId][keyId];
|
key_state_t *keyState = &KeyStates[slotId][keyId];
|
||||||
if (keyState->current && !keyState->suppressed) {
|
if (keyState->current) {
|
||||||
key_action_t action = CurrentKeymap[LayerId_Base][slotId][keyId];
|
key_action_t action = CurrentKeymap[LayerId_Base][slotId][keyId];
|
||||||
if (action.type == KeyActionType_SwitchLayer) {
|
if (action.type == KeyActionType_SwitchLayer) {
|
||||||
if (action.switchLayer.mode != SwitchLayerMode_Toggle) {
|
if (action.switchLayer.mode != SwitchLayerMode_Toggle) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#include "led_display.h"
|
#include "led_display.h"
|
||||||
#include "usb_composite_device.h"
|
#include "usb_composite_device.h"
|
||||||
#include "usb_report_updater.h"
|
|
||||||
|
|
||||||
static usb_basic_keyboard_report_t usbBasicKeyboardReports[2];
|
static usb_basic_keyboard_report_t usbBasicKeyboardReports[2];
|
||||||
uint32_t UsbBasicKeyboardActionCounter;
|
uint32_t UsbBasicKeyboardActionCounter;
|
||||||
@@ -45,7 +44,6 @@ usb_status_t UsbBasicKeyboardCallback(class_handle_t handle, uint32_t event, voi
|
|||||||
switch (event) {
|
switch (event) {
|
||||||
// This event is received when the report has been sent
|
// This event is received when the report has been sent
|
||||||
case kUSB_DeviceHidEventSendResponse:
|
case kUSB_DeviceHidEventSendResponse:
|
||||||
UsbReportUpdateSemaphore &= ~(1 << USB_BASIC_KEYBOARD_INTERFACE_INDEX);
|
|
||||||
if (UsbCompositeDevice.attach) {
|
if (UsbCompositeDevice.attach) {
|
||||||
error = kStatus_USB_Success;
|
error = kStatus_USB_Success;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#include "usb_composite_device.h"
|
#include "usb_composite_device.h"
|
||||||
#include "usb_report_updater.h"
|
|
||||||
|
|
||||||
uint32_t UsbMediaKeyboardActionCounter;
|
uint32_t UsbMediaKeyboardActionCounter;
|
||||||
static usb_media_keyboard_report_t usbMediaKeyboardReports[2];
|
static usb_media_keyboard_report_t usbMediaKeyboardReports[2];
|
||||||
@@ -43,7 +42,6 @@ usb_status_t UsbMediaKeyboardCallback(class_handle_t handle, uint32_t event, voi
|
|||||||
switch (event) {
|
switch (event) {
|
||||||
// This event is received when the report has been sent
|
// This event is received when the report has been sent
|
||||||
case kUSB_DeviceHidEventSendResponse:
|
case kUSB_DeviceHidEventSendResponse:
|
||||||
UsbReportUpdateSemaphore &= ~(1 << USB_MEDIA_KEYBOARD_INTERFACE_INDEX);
|
|
||||||
if (UsbCompositeDevice.attach) {
|
if (UsbCompositeDevice.attach) {
|
||||||
error = kStatus_USB_Success;
|
error = kStatus_USB_Success;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#include "usb_composite_device.h"
|
#include "usb_composite_device.h"
|
||||||
#include "usb_report_updater.h"
|
|
||||||
|
|
||||||
uint32_t UsbMouseActionCounter;
|
uint32_t UsbMouseActionCounter;
|
||||||
static usb_mouse_report_t usbMouseReports[2];
|
static usb_mouse_report_t usbMouseReports[2];
|
||||||
@@ -43,7 +42,6 @@ usb_status_t UsbMouseCallback(class_handle_t handle, uint32_t event, void *param
|
|||||||
switch (event) {
|
switch (event) {
|
||||||
// This event is received when the report has been sent
|
// This event is received when the report has been sent
|
||||||
case kUSB_DeviceHidEventSendResponse:
|
case kUSB_DeviceHidEventSendResponse:
|
||||||
UsbReportUpdateSemaphore &= ~(1 << USB_MOUSE_INTERFACE_INDEX);
|
|
||||||
if (UsbCompositeDevice.attach) {
|
if (UsbCompositeDevice.attach) {
|
||||||
error = kStatus_USB_Success;
|
error = kStatus_USB_Success;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#include "usb_composite_device.h"
|
#include "usb_composite_device.h"
|
||||||
#include "usb_report_updater.h"
|
|
||||||
|
|
||||||
uint32_t UsbSystemKeyboardActionCounter;
|
uint32_t UsbSystemKeyboardActionCounter;
|
||||||
static usb_system_keyboard_report_t usbSystemKeyboardReports[2];
|
static usb_system_keyboard_report_t usbSystemKeyboardReports[2];
|
||||||
@@ -43,7 +42,6 @@ usb_status_t UsbSystemKeyboardCallback(class_handle_t handle, uint32_t event, vo
|
|||||||
switch (event) {
|
switch (event) {
|
||||||
// This event is received when the report has been sent
|
// This event is received when the report has been sent
|
||||||
case kUSB_DeviceHidEventSendResponse:
|
case kUSB_DeviceHidEventSendResponse:
|
||||||
UsbReportUpdateSemaphore &= ~(1 << USB_SYSTEM_KEYBOARD_INTERFACE_INDEX);
|
|
||||||
if (UsbCompositeDevice.attach) {
|
if (UsbCompositeDevice.attach) {
|
||||||
error = kStatus_USB_Success;
|
error = kStatus_USB_Success;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ static uint16_t DoubleTapSwitchLayerReleaseTimeout = 200;
|
|||||||
|
|
||||||
static bool activeMouseStates[ACTIVE_MOUSE_STATES_COUNT];
|
static bool activeMouseStates[ACTIVE_MOUSE_STATES_COUNT];
|
||||||
bool TestUsbStack = false;
|
bool TestUsbStack = false;
|
||||||
bool KeymapChanged = false;
|
|
||||||
static uint8_t layerCache[SLOT_COUNT][MAX_KEY_COUNT_PER_MODULE];
|
|
||||||
|
|
||||||
volatile uint8_t UsbReportUpdateSemaphore = 0;
|
|
||||||
|
|
||||||
mouse_kinetic_state_t MouseMoveState = {
|
mouse_kinetic_state_t MouseMoveState = {
|
||||||
.isScroll = false,
|
.isScroll = false,
|
||||||
@@ -335,8 +331,7 @@ static void updateActiveUsbReports(void)
|
|||||||
if (layerChanged) {
|
if (layerChanged) {
|
||||||
stickyModifiers = 0;
|
stickyModifiers = 0;
|
||||||
}
|
}
|
||||||
bool keymapChangedLastCycle = KeymapChanged;
|
bool layerGotReleased = layerChanged && activeLayer == LayerId_Base;
|
||||||
KeymapChanged = false;
|
|
||||||
LedDisplay_SetLayer(activeLayer);
|
LedDisplay_SetLayer(activeLayer);
|
||||||
|
|
||||||
if (TestUsbStack) {
|
if (TestUsbStack) {
|
||||||
@@ -361,7 +356,7 @@ static void updateActiveUsbReports(void)
|
|||||||
for (uint8_t slotId=0; slotId<SLOT_COUNT; slotId++) {
|
for (uint8_t slotId=0; slotId<SLOT_COUNT; slotId++) {
|
||||||
for (uint8_t keyId=0; keyId<MAX_KEY_COUNT_PER_MODULE; keyId++) {
|
for (uint8_t keyId=0; keyId<MAX_KEY_COUNT_PER_MODULE; keyId++) {
|
||||||
key_state_t *keyState = &KeyStates[slotId][keyId];
|
key_state_t *keyState = &KeyStates[slotId][keyId];
|
||||||
key_action_t *action;
|
key_action_t *action = &CurrentKeymap[activeLayer][slotId][keyId];
|
||||||
|
|
||||||
if (keyState->debouncing) {
|
if (keyState->debouncing) {
|
||||||
if ((uint8_t)(Timer_GetCurrentTime() - keyState->timestamp) > (keyState->previous ? DebounceTimePress : DebounceTimeRelease)) {
|
if ((uint8_t)(Timer_GetCurrentTime() - keyState->timestamp) > (keyState->previous ? DebounceTimePress : DebounceTimeRelease)) {
|
||||||
@@ -374,24 +369,19 @@ static void updateActiveUsbReports(void)
|
|||||||
keyState->debouncing = true;
|
keyState->debouncing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyState->current && !keyState->previous) {
|
if (keyState->current) {
|
||||||
if (SleepModeActive) {
|
if (SleepModeActive && !keyState->previous) {
|
||||||
WakeUpHost();
|
WakeUpHost();
|
||||||
}
|
}
|
||||||
if (secondaryRoleState == SecondaryRoleState_Pressed) {
|
key_action_t *baseAction = &CurrentKeymap[LayerId_Base][slotId][keyId];
|
||||||
|
if (layerGotReleased && !(baseAction->type == KeyActionType_Keystroke && baseAction->keystroke.scancode == 0 && baseAction->keystroke.modifiers)) {
|
||||||
|
keyState->suppressed = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger secondary role.
|
// Trigger secondary role.
|
||||||
|
if (!keyState->previous && secondaryRoleState == SecondaryRoleState_Pressed) {
|
||||||
secondaryRoleState = SecondaryRoleState_Triggered;
|
secondaryRoleState = SecondaryRoleState_Triggered;
|
||||||
keyState->current = false;
|
keyState->current = false;
|
||||||
} else {
|
|
||||||
layerCache[slotId][keyId] = activeLayer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
action = &CurrentKeymap[layerCache[slotId][keyId]][slotId][keyId];
|
|
||||||
|
|
||||||
if (keyState->current) {
|
|
||||||
if ((KeymapChanged || keymapChangedLastCycle) && keyState->previous) {
|
|
||||||
keyState->suppressed = true;
|
|
||||||
} else if (action->type == KeyActionType_Keystroke && action->keystroke.secondaryRole) {
|
} else if (action->type == KeyActionType_Keystroke && action->keystroke.secondaryRole) {
|
||||||
// Press released secondary role key.
|
// Press released secondary role key.
|
||||||
if (!keyState->previous && secondaryRoleState == SecondaryRoleState_Released) {
|
if (!keyState->previous && secondaryRoleState == SecondaryRoleState_Released) {
|
||||||
@@ -405,7 +395,9 @@ static void updateActiveUsbReports(void)
|
|||||||
applyKeyAction(keyState, action);
|
applyKeyAction(keyState, action);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (keyState->suppressed) {
|
||||||
keyState->suppressed = false;
|
keyState->suppressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Release secondary role key.
|
// Release secondary role key.
|
||||||
if (keyState->previous && secondaryRoleSlotId == slotId && secondaryRoleKeyId == keyId) {
|
if (keyState->previous && secondaryRoleSlotId == slotId && secondaryRoleKeyId == keyId) {
|
||||||
@@ -443,10 +435,6 @@ void UpdateUsbReports(void)
|
|||||||
KeyStates[SlotId_RightKeyboardHalf][keyId].current = RightKeyMatrix.keyStates[keyId];
|
KeyStates[SlotId_RightKeyboardHalf][keyId].current = RightKeyMatrix.keyStates[keyId];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UsbReportUpdateSemaphore && !SleepModeActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UsbReportUpdateCounter++;
|
UsbReportUpdateCounter++;
|
||||||
|
|
||||||
ResetActiveUsbBasicKeyboardReport();
|
ResetActiveUsbBasicKeyboardReport();
|
||||||
@@ -462,32 +450,20 @@ void UpdateUsbReports(void)
|
|||||||
bool HasUsbMouseReportChanged = memcmp(ActiveUsbMouseReport, GetInactiveUsbMouseReport(), sizeof(usb_mouse_report_t)) != 0;
|
bool HasUsbMouseReportChanged = memcmp(ActiveUsbMouseReport, GetInactiveUsbMouseReport(), sizeof(usb_mouse_report_t)) != 0;
|
||||||
|
|
||||||
if (HasUsbBasicKeyboardReportChanged) {
|
if (HasUsbBasicKeyboardReportChanged) {
|
||||||
usb_status_t status = UsbBasicKeyboardAction();
|
UsbBasicKeyboardAction();
|
||||||
if (status == kStatus_USB_Success) {
|
|
||||||
UsbReportUpdateSemaphore |= 1 << USB_BASIC_KEYBOARD_INTERFACE_INDEX;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HasUsbMediaKeyboardReportChanged) {
|
if (HasUsbMediaKeyboardReportChanged) {
|
||||||
usb_status_t status = UsbMediaKeyboardAction();
|
UsbMediaKeyboardAction();
|
||||||
if (status == kStatus_USB_Success) {
|
|
||||||
UsbReportUpdateSemaphore |= 1 << USB_MEDIA_KEYBOARD_INTERFACE_INDEX;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HasUsbSystemKeyboardReportChanged) {
|
if (HasUsbSystemKeyboardReportChanged) {
|
||||||
usb_status_t status = UsbSystemKeyboardAction();
|
UsbSystemKeyboardAction();
|
||||||
if (status == kStatus_USB_Success) {
|
|
||||||
UsbReportUpdateSemaphore |= 1 << USB_SYSTEM_KEYBOARD_INTERFACE_INDEX;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send out the mouse position and wheel values continuously if the report is not zeros, but only send the mouse button states when they change.
|
// Send out the mouse position and wheel values continuously if the report is not zeros, but only send the mouse button states when they change.
|
||||||
if (HasUsbMouseReportChanged || ActiveUsbMouseReport->x || ActiveUsbMouseReport->y ||
|
if (HasUsbMouseReportChanged || ActiveUsbMouseReport->x || ActiveUsbMouseReport->y ||
|
||||||
ActiveUsbMouseReport->wheelX || ActiveUsbMouseReport->wheelY) {
|
ActiveUsbMouseReport->wheelX || ActiveUsbMouseReport->wheelY) {
|
||||||
usb_status_t status = UsbMouseAction();
|
UsbMouseAction();
|
||||||
if (status == kStatus_USB_Success) {
|
|
||||||
UsbReportUpdateSemaphore |= 1 << USB_MOUSE_INTERFACE_INDEX;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,9 +70,7 @@
|
|||||||
extern mouse_kinetic_state_t MouseMoveState;
|
extern mouse_kinetic_state_t MouseMoveState;
|
||||||
extern mouse_kinetic_state_t MouseScrollState;
|
extern mouse_kinetic_state_t MouseScrollState;
|
||||||
extern uint32_t UsbReportUpdateCounter;
|
extern uint32_t UsbReportUpdateCounter;
|
||||||
extern volatile uint8_t UsbReportUpdateSemaphore;
|
|
||||||
extern bool TestUsbStack;
|
extern bool TestUsbStack;
|
||||||
extern bool KeymapChanged;
|
|
||||||
|
|
||||||
// Functions:
|
// Functions:
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"commander": "^2.11.0",
|
"commander": "^2.11.0",
|
||||||
"shelljs": "^0.7.8"
|
"shelljs": "^0.7.8"
|
||||||
},
|
},
|
||||||
"firmwareVersion": "8.4.5",
|
"firmwareVersion": "8.4.4",
|
||||||
"deviceProtocolVersion": "4.4.0",
|
"deviceProtocolVersion": "4.4.0",
|
||||||
"moduleProtocolVersion": "4.0.0",
|
"moduleProtocolVersion": "4.0.0",
|
||||||
"userConfigVersion": "4.1.0",
|
"userConfigVersion": "4.1.0",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
#define FIRMWARE_MAJOR_VERSION 8
|
#define FIRMWARE_MAJOR_VERSION 8
|
||||||
#define FIRMWARE_MINOR_VERSION 4
|
#define FIRMWARE_MINOR_VERSION 4
|
||||||
#define FIRMWARE_PATCH_VERSION 5
|
#define FIRMWARE_PATCH_VERSION 4
|
||||||
|
|
||||||
#define DEVICE_PROTOCOL_MAJOR_VERSION 4
|
#define DEVICE_PROTOCOL_MAJOR_VERSION 4
|
||||||
#define DEVICE_PROTOCOL_MINOR_VERSION 4
|
#define DEVICE_PROTOCOL_MINOR_VERSION 4
|
||||||
|
|||||||
Reference in New Issue
Block a user