At least on Windows, it really seems there is no way to toggle these keys when the workstation is locked. No matter what language or framework is in use, they all need to pass through the underlying OS layer. Without an interactive session, these key presses are not sent.
Naturally, if you are standing at the keyboard and pressing these keys, they respond just fine.
So, this CAN be done, by controlling a keyboard as an attached peripheral, such as Arduino. Some of these models can act as a USB keyboard/mouse. (I have tried this using both the Arduino Leonardo and Spark Fun Pro Micro. Both respond the same way for this use case.)
NOTE: even when toggle keys are updated by Arduino or by a person at the terminal, the toggle key states are NOT updated in the operating system until the workstation is unlocked. Whatever state a toggle key has when the workstation is locked, regardless of what the person at the locked terminal does with the keyboard, that key will continue to appear in that state to any running scripts until the moment the workstation is unlocked. This can be easily verified using the AHK script below.
Below is a minimal example of the AutoHotKey control script (although any program that can send data over a serial connection will do just as well), and the Arduino sketch:
AutoHotKey control script
Loop
{
RS232_FileHandle := RS232_Initialize()
if (RS232_FileHandle)
{
; Turn them all off
(1 = GetKeyState("NumLock", "T")) ? RS232_Write(RS232_FileHandle, "219") : NA
Sleep, 750
(1 = GetKeyState("CapsLock", "T")) ? RS232_Write(RS232_FileHandle, "193") : NA
Sleep, 750
(1 = GetKeyState("ScrollLock", "T")) ? RS232_Write(RS232_FileHandle, "207") : NA
Sleep, 4000
; Turn them all on
(0 = GetKeyState("NumLock", "T")) ? RS232_Write(RS232_FileHandle, "219") : NA
Sleep, 750
(0 = GetKeyState("CapsLock", "T")) ? RS232_Write(RS232_FileHandle, "193") : NA
Sleep, 750
(0 = GetKeyState("ScrollLock", "T")) ? RS232_Write(RS232_FileHandle, "207") : NA
RS232_Close(RS232_FileHandle)
}
Sleep, 4000
}
RS232_LoadSettings()
{
RS232_Port := "COM3"
RS232_Baud := "9600"
RS232_Parity := "N"
RS232_DataBits := "8"
RS232_StopBits := "1"
RS232_Timeout := "Off"
RS232_XonXoff := "Off"
RS232_CTS_Hand := "Off"
RS232_DSR_Hand := "Off"
RS232_DSR_Sens := "Off"
RS232_DTR := "Off"
RS232_RTS := "Off"
; MSDN Reference: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/mode
RS232_Settings = %RS232_Port%:BAUD=%RS232_Baud% PARITY=%RS232_Parity% DATA=%RS232_DataBits% STOP=%RS232_StopBits% to=%RS232_Timeout% xon=%RS232_XonXoff% odsr=%RS232_DSR_Hand% octs=%RS232_CTS_Hand% dtr=%RS232_DTR% rts=%RS232_RTS% idsr=%RS232_DSR_Sens%
return RS232_Settings
}
RS232_Initialize()
{
; Source adapted from: https://autohotkey.com/board/topic/26231-serial-com-port-console-script/
RS232_Settings := RS232_LoadSettings()
RS232_Port := StrSplit(RS232_Settings, ":")[1]
RS232_COM := (4 <= StrLen(RS232_Port) ? "\\.\" : "") . RS232_Port
StringTrimLeft, RS232_Settings, RS232_Settings, StrLen(RS232_Port)+1
VarSetCapacity(DCB, 28)
if (1 <> DllCall("BuildCommDCB","str",RS232_Settings,"UInt",&DCB))
{
return false
}
hCom := DllCall("CreateFile","Str",RS232_COM,"UInt",0xC0000000,"UInt",3,"UInt",0,"UInt",3,"UInt",0,"UInt",0,"Cdecl Int")
if (hCom < 1)
{
return false
}
if (1 <> DllCall("SetCommState","UInt",hCom,"UInt",&DCB))
{
RS232_Close(hCom)
return false
}
VarSetCapacity(Data, 20, 0)
NumPut(0xffffffff, Data, 0, "UInt")
NumPut(0x00000000, Data, 4, "UInt")
NumPut(0x00000000, Data, 8, "UInt")
NumPut(0x00000000, Data, 12, "UInt")
NumPut(0x00000000, Data, 16, "UInt")
if (1 <> DllCall("SetCommTimeouts","UInt",hCom,"UInt",&Data))
{
RS232_Close(hCom)
return false
}
return hCom
}
RS232_Write(hCom, msg)
{
SetFormat, Integer, DEC
StringSplit, Byte, msg, `,
Data_Length := Byte0
VarSetCapacity(Data, Byte0, 0xFF)
i := 1
Loop %Byte0%
{
NumPut(Byte%i%, Data, (i-1) , "UChar")
i++
}
Bytes_Sent := 0
WF_Result := DllCall("WriteFile","UInt",hCom,"UInt",&Data,"UInt",Data_Length,"UInt*",Bytes_Sent,"Int","NULL")
if (WF_Result <> 1 or Bytes_Sent <> Data_Length)
{
return false
}
return Bytes_Sent
}
RS232_Close(hCom)
{
return (1 == DllCall("CloseHandle","UInt",hCom))
}
Arduino sketch
/* Pro Micro NumCapsScrollToggleDemo
by: Jonathan David Arndt
date: March 6, 2020
This will allow the toggle of the Num Lock, Caps Lock, and Scroll Lock keys
on the keyboard, via commands sent over USB serial
*/
#include <Keyboard.h>
// You could patch this into your Keyboard.h file, or just define it here
// Source: https://forum.arduino.cc/index.php?topic=173583.0 (attachment: USBAPI.h)
#define KEY_NUM_LOCK 0xDB
#define KEY_SCROLL_LOCK 0xCF
void pressAndRelease(int c);
void setup()
{
Serial.begin(9600); // This pipes to the serial monitor
delay(3000); // Wait a moment for things to get setup
Serial.println("Initialize Serial Monitor");
}
void loop()
{
int c = 0;
if (0 < Serial.available())
{
c = Serial.read();
if (219 == c)
{
pressAndRelease(KEY_NUM_LOCK);
}
else if (193 == c)
{
pressAndRelease(KEY_CAPS_LOCK);
}
else if (207 == c)
{
pressAndRelease(KEY_SCROLL_LOCK);
}
}
}
void pressAndRelease(int c)
{
Keyboard.press(c);
Keyboard.release(c);
}