marketplace/utility

KMBox Net

Let programs written for a KMBox Net send keyboard and mouse input through Rebind Link over your network.

@rebindv0.1.1free

More install options
$rebind install @rebind/kmbox-net

Gallery

KMBox Net preview

Readme

Use programs made for a KMBox Net with Rebind Link. The program runs on another computer and needs no changes.

Needs Rebind 3.5.5 or newer and Rebind Link on Windows or macOS. Tested on Windows and macOS; not yet tested with a program on a second computer.

Set up

  1. Open the package in Rebind and select Options.
  2. This computer's address: this computer's network address, such as 192.168.2.188.
  3. Port and Device code: the same values your program uses.
  4. Controlling computer's address: the address of the computer running the program. Separate several with commas.
  5. Start the package. Restart it after changing an option.

If your program expects a fixed address such as 192.168.2.188, give this computer that address or change it in the program.

What works

  • Mouse movement, buttons, and scrolling
  • Keyboard input
  • Smooth and curved moves, both as a straight line over the requested time
  • Encrypted commands
  • Monitor, so the program can see your physical mouse and keyboard

Blocking your physical mouse or keyboard (mask) and device settings such as reboot are not supported. The program's requests are accepted but have no effect.

Safety

Anyone on your network who knows the device code and sends from an allowed address can control this computer's keyboard and mouse. The code is not a password. Use a network you trust, and stop the package when you are not using it. On Windows, Rebind is allowed through the firewall on all networks.

Stop

Stop the package from the editor toolbar, or press Left Ctrl + Left Alt + K to stop all scripts. Held keys and buttons are released.

Reviews

No reviews yet. Write a review in the app.

Source

main.luau

--[[ KMBox Net

  Answers the KMBox Net UDP protocol so programs written for that device can
  send keyboard and mouse input through Rebind Link. Wire contract: the
  vendor's NetConfig/kmboxNet.h and kmboxNet.cpp.

  Every packet starts with four little-endian u32s: device code, rand,
  sequence, command. Clients send one command and wait for any reply, so every
  well-formed command with the right code is answered by echoing that header,
  and anything else is ignored. Automove and bezier are answered when the move
  finishes, as the vendor demo times them.
]]

local xxtea = require("./xxtea")

local cfg = UI.Schema({
  bind_ip = UI.Text("", {
    label = "This computer's address",
    placeholder = "e.g. 192.168.2.188",
    tooltip = "The network address your program sends to. Restart after changing.",
  }),
  port = UI.Slider(8888, {
    label = "Port",
    min = 1024,
    max = 65535,
    step = 1,
    tooltip = "The port your program sends to. Restart after changing.",
  }),
  device_code = UI.Text("", {
    label = "Device code",
    placeholder = "8 hex digits, e.g. AF425414",
    tooltip = "The code your program is configured with. Restart after changing.",
  }),
  controller_ip = UI.Text("", {
    label = "Controlling computer's address",
    placeholder = "e.g. 192.168.2.10",
    tooltip = "Only these addresses are answered. Separate several with commas. Restart after changing.",
  }),
})

local CMD_MOUSE_MOVE = 0xAEDE7345
local CMD_MOUSE_LEFT = 0x9823AE8D
local CMD_MOUSE_MIDDLE = 0x97A3AE8D
local CMD_MOUSE_RIGHT = 0x238D8212
local CMD_MOUSE_WHEEL = 0xFFEEAD38
local CMD_AUTOMOVE = 0xAEDE7346
local CMD_BEZIER = 0xA238455A
local CMD_KEYBOARD = 0x123C2C2F
local CMD_MONITOR = 0x27388020

local MOUSE = {
  [CMD_MOUSE_MOVE] = "move",
  [CMD_MOUSE_LEFT] = "buttons",
  [CMD_MOUSE_MIDDLE] = "buttons",
  [CMD_MOUSE_RIGHT] = "buttons",
  [CMD_MOUSE_WHEEL] = "buttons",
  [CMD_AUTOMOVE] = "timed",
  [CMD_BEZIER] = "timed",
}

local HEADER_BYTES = 16
local MOUSE_BYTES = 72 -- header + soft_mouse_t
local KEYBOARD_BYTES = 28 -- header + soft_keyboard_t
local ENCRYPTED_BYTES = 128
local MONITOR_ON = 0xAA55
local MAX_MOVE_MS = 10000

local socket = nil
local code = 0
local key = nil
local allowed = {}
local held_buttons = {} -- button number (1-5) -> true
local held_keys = {} -- HID usage -> true
local skipped = {} -- usages already reported as unsupported
local repress = {} -- held inputs physically released since the last tick
local monitor = nil -- { ip, port } while the client wants reports
local motion_x, motion_y, motion_wheel = 0, 0, 0
local last_report = nil

local function set_buttons(mask)
  for n = 1, 5 do
    local want = bit32.btest(mask, bit32.lshift(1, n - 1))
    if want ~= (held_buttons[n] == true) then
      if want then
        HID.Down(`Mouse{n}`)
      else
        HID.Up(`Mouse{n}`)
      end
      held_buttons[n] = want or nil
    end
  end
end

local function press_usage(usage, down)
  local ok, err = pcall(down and HID.Down or HID.Up, `usage:{usage}`)
  if not ok then
    if not skipped[usage] then
      skipped[usage] = true
      Log.Warn(`KMBox Net: skipped key usage {usage}: {err}`)
    end
    return
  end
  held_keys[usage] = down or nil
end

-- apply a soft_keyboard_t snapshot: ctrl bits for 0xE0-0xE7, then 10 usages
local function set_keys(data)
  local want = {}
  local ctrl = string.byte(data, HEADER_BYTES + 1)
  for bit = 0, 7 do
    if bit32.btest(ctrl, bit32.lshift(1, bit)) then
      want[0xE0 + bit] = true
    end
  end
  for i = HEADER_BYTES + 3, KEYBOARD_BYTES do
    local usage = string.byte(data, i)
    if usage ~= 0 then
      want[usage] = true
    end
  end
  for usage in pairs(held_keys) do
    if not want[usage] then
      press_usage(usage, false)
    end
  end
  for usage in pairs(want) do
    if not held_keys[usage] then
      press_usage(usage, true)
    end
  end
end

-- move (dx, dy) evenly over ms, then call done
local function timed_move(dx, dy, ms, done)
  Run(function()
    local start = os.clock()
    local sent_x, sent_y = 0, 0
    local t = 0
    while t < 1 do
      Sleep(1)
      t = if ms > 0 then math.min(1, (os.clock() - start) * 1000 / ms) else 1
      local x, y = math.round(dx * t), math.round(dy * t)
      if x ~= sent_x or y ~= sent_y then
        HID.Move(x - sent_x, y - sent_y)
        sent_x, sent_y = x, y
      end
    end
    done()
  end)
end

local function handle(data, ip, port)
  if #data == ENCRYPTED_BYTES then
    data = xxtea.decrypt(data, key)
  end
  if #data < HEADER_BYTES then
    return
  end
  local device, rand, _, cmd = string.unpack("<I4I4I4I4", data)
  if device ~= code then
    return
  end
  local head = string.sub(data, 1, HEADER_BYTES)
  local function reply()
    socket:SendTo(ip, port, head)
  end

  local kind = MOUSE[cmd]
  if kind then
    if #data < MOUSE_BYTES then
      return
    end
    -- soft_mouse_t: button, x, y, wheel. clients that track no state send
    -- button 0 with moves, so the mask is applied only on button commands.
    local button, x, y, wheel =
      string.unpack("<i4i4i4i4", data, HEADER_BYTES + 1)
    if kind == "timed" then
      timed_move(x, y, math.min(rand, MAX_MOVE_MS), reply)
      return
    end
    if kind == "buttons" then
      set_buttons(button)
    end
    if x ~= 0 or y ~= 0 then
      HID.Move(x, y)
    end
    if wheel ~= 0 then
      HID.Scroll(-wheel) -- positive wheel scrolls down on the device
    end
  elseif cmd == CMD_KEYBOARD then
    if #data < KEYBOARD_BYTES then
      return
    end
    set_keys(data)
  elseif cmd == CMD_MONITOR then
    -- the vendor passes the port as a short, so ports from 32768 arrive
    -- sign-extended with 0xFFFF in the high half instead of 0xAA55
    local high = bit32.rshift(rand, 16)
    if rand ~= 0 and (high == MONITOR_ON or high == 0xFFFF) then
      monitor = { ip = ip, port = bit32.band(rand, 0xFFFF) }
      last_report = nil
    else
      monitor = nil
    end
  end
  -- connect, mask, reboot, config, display and unknown commands are
  -- acknowledged without effect; see README.md
  reply()
end

local function parse_config()
  local ip = cfg.bind_ip:match("^%s*(.-)%s*$")
  if ip == "" or ip == "0.0.0.0" or ip == "::" then
    error(
      "KMBox Net: set This computer's address to one of this computer's network addresses"
    )
  end
  local hex = cfg.device_code:match("^%s*(%x%x%x%x%x%x%x%x)%s*$")
  if not hex then
    error("KMBox Net: set Device code to the 8 hex digits your program uses")
  end
  local controllers = {}
  for addr in cfg.controller_ip:gmatch("[^,%s]+") do
    controllers[addr] = true
  end
  if next(controllers) == nil then
    error("KMBox Net: set Controlling computer's address")
  end
  return ip, tonumber(hex, 16), controllers
end

function OnStart()
  local ip, device, controllers = parse_config()
  code, key, allowed = device, xxtea.key_for(device), controllers
  socket = Net.UDPListen(ip, cfg.port, function(data, from, port)
    if not allowed[from] then
      return
    end
    local ok, err = pcall(handle, data, from, port)
    if not ok then
      Log.Warn(`KMBox Net: {err}`)
    end
  end)
  Log.Info(`KMBox Net listening on {ip}:{socket.port}`)
  UI.Notify(`KMBox Net: {ip}:{socket.port}`, "success")
end

function OnStop()
  if socket then
    socket:Stop()
    socket = nil
  end
  held_buttons, held_keys, skipped, repress = {}, {}, {}, {}
  monitor, last_report = nil, nil
end

-- a physical release of a key or button this package holds releases it on
-- Rebind Link too. The relay may send that release after this hook runs, so
-- the press is restored on the next tick.
function OnInputUp(name)
  repress[name] = true
end

local function restore_held()
  for name in pairs(repress) do
    local button = tonumber(name:match("^Mouse(%d)$"))
    local usage = HID.Usage(name)
    if button and held_buttons[button] then
      HID.Down(name)
    elseif usage and held_keys[usage] then
      HID.Down(`usage:{usage}`)
    end
  end
  table.clear(repress)
end

function OnInputMove(dx, dy)
  motion_x += dx
  motion_y += dy
end

-- reported as the physical mouse sends it: positive scrolls up. Commands use
-- the opposite sign (vendor manual); that the device forwards the physical
-- report unflipped is an unverified assumption.
function OnInputScroll(delta)
  motion_wheel += delta
end

local function clamp16(n)
  return math.clamp(n, -32768, 32767)
end

-- monitor: 8-byte mouse report and 12-byte keyboard report of physical input
function OnTick()
  restore_held()
  if not monitor then
    motion_x, motion_y, motion_wheel = 0, 0, 0
    return
  end
  local buttons = 0
  for n = 1, 5 do
    if Input.IsDown(`Mouse{n}`) then
      buttons = bit32.bor(buttons, bit32.lshift(1, n - 1))
    end
  end
  local mods, keys = 0, {}
  for _, name in Input.GetActiveKeys() do
    local usage = HID.Usage(name)
    if usage and usage >= 0xE0 then
      mods = bit32.bor(mods, bit32.lshift(1, usage - 0xE0))
    elseif usage and #keys < 10 then
      table.insert(keys, usage)
    end
  end
  table.sort(keys)
  for i = #keys + 1, 10 do
    keys[i] = 0
  end
  local report = string.pack(
    "<BBhhhBB" .. string.rep("B", 10),
    1,
    buttons,
    clamp16(motion_x),
    clamp16(motion_y),
    clamp16(motion_wheel),
    1,
    mods,
    table.unpack(keys)
  )
  local moved = motion_x ~= 0 or motion_y ~= 0 or motion_wheel ~= 0
  motion_x, motion_y, motion_wheel = 0, 0, 0
  -- movement is a delta, so a repeat of the same motion is still news
  if moved or report ~= last_report then
    last_report = report
    socket:SendTo(monitor.ip, monitor.port, report)
  end
end