This commit is contained in:
Mike Trudeau
2026-02-13 20:33:17 +00:00
parent 008f60446f
commit 2d168f4a3a
64 changed files with 11570 additions and 0 deletions

53
app/Makefile Normal file
View File

@@ -0,0 +1,53 @@
##
##
##
.PHONY: help all linux darwin windows cli
help: ## This help.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
.DEFAULT_GOAL := all
bin/:
@mkdir -p bin
all: ## build linux, darwin & windows
all: linux darwin windows
cli: linux-cli windows-cli darwin-cli
linux: linux-cli
windows: windows-cli
darwin: darwin-cli
linux-cli: bin/ ## build linux binary
@GOOS=linux GOARCH=amd64 CGO_ENABLED=1 \
go build \
-installsuffix cgo -ldflags=" -s -w" \
-o bin/beeon-cli-linux-amd64 \
microjelly.com/beeon/app/cmd/cli
darwin-cli: bin/ ## build darwin binary
@GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 \
CC=o64-clang CXX=o64-clang++ \
LDFLAGS="-linkmode external -s" \
go build \
-installsuffix cgo -ldflags=" -s -w" \
-o bin/beeon-cli-darwin-amd64 \
microjelly.com/beeon/app/cmd/cli
windows-cli: bin/ ## build windows binary
@GOOS=windows GOARCH=amd64 CGO_ENABLED=1 \
CC=x86_64-w64-mingw32-gcc \
go build \
-installsuffix cgo -ldflags=" -s -w" \
-o bin/beeon-cli-windows-amd64.exe \
microjelly.com/beeon/app/cmd/cli
mod-tidy:
@go clean --modcache
@GOPROXY=direct GOSUMDB=off go mod tidy
clean:
@rm -rf bin

3
app/build/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
Dockerfile.dapper[0-9]*
.dapper
bin/

View File

@@ -0,0 +1,10 @@
FROM dockercore/golang-cross:latest
RUN apt-get update -qq \
&& apt-get install -y -q --no-install-recommends libusb-1.0-0-dev
ENV DAPPER_SOURCE /source
ENV DAPPER_OUTPUT ./build/bin
WORKDIR ${DAPPER_SOURCE}
ENTRYPOINT ["./build/scripts/entry"]
CMD ["cli"]

22
app/build/Makefile Normal file
View File

@@ -0,0 +1,22 @@
TARGETS := $(shell ls scripts)
.dapper:
@echo Downloading dapper
@curl -sL https://releases.rancher.com/dapper/latest/dapper-`uname -s`-`uname -m` > .dapper.tmp
@@chmod +x .dapper.tmp
@./.dapper.tmp -v
@mv .dapper.tmp .dapper
$(TARGETS): .dapper
cd ../ && $(PWD)/.dapper -f build/Dockerfile.dapper $@
@yes | docker image prune > /dev/null
.PHONY: $(TARGETS)
.DEFAULT_GOAL := cli
clean:
@rm -rf $(PWD)/bin $(PWD)/.dapper $(PWD)/Dockerfile.dapper[0-9]*
@docker rmi app:master
shell-bind: .dapper
cd ../ && $(PWD)/.dapper -f build/Dockerfile.dapper -m bind -s

7
app/build/scripts/cli Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -e
## multi-platform cli build
make cli
cp bin/*cli* ${DAPPER_OUTPUT}/
ls -la ${DAPPER_OUTPUT}/*cli*

13
app/build/scripts/entry Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
mkdir -p ${DAPPER_OUTPUT}
if [ -e ${DIR}/$1 ]; then
${DIR}/"$@"
else
exec "$@"
fi
chown -R $DAPPER_UID:$DAPPER_GID ${DAPPER_OUTPUT}

208
app/cmd/cli/cli.go Normal file
View File

@@ -0,0 +1,208 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"strings"
"microjelly.com/beeon/app/pkg/device"
"microjelly.com/beeon/app/pkg/device/keycodes"
"microjelly.com/beeon/app/pkg/device/report3"
)
var (
appVersion string = "1.01"
vendorID uint16 = 0x16d0
productID uint16 = 0x09fa
)
func main() {
var (
action string = "none"
serialNumber string = ""
inState string = ""
newState report3.State = report3.StateUnset
inRate string = ""
newRate report3.Rate = report3.RateUnset
newMode report3.Mode = report3.ModeUnset
inMouseMode string = ""
newMouseMode report3.MouseMode = report3.MouseModeUnset
inKeyCode uint = 256
newKeyCode keycodes.KeyCode = keycodes.KeyUnset
)
debugFlag := flag.Bool("debug", false, "enable debug logging")
listFlag := flag.Bool("list", false, "list connected devices")
readFlag := flag.Bool("read", false, "read device config")
flag.StringVar(&serialNumber, "serial", "", "target specific device by serial_number")
flag.StringVar(&inState, "state", "", "change device state <idle|active>")
flag.StringVar(&inRate, "rate", "", "change device rate <extra-slow|slow|normal|fast>")
flag.StringVar(&inMouseMode, "mouse", "", "change to mouse mode and set mode <circle|random>")
flag.UintVar(&inKeyCode, "keyboard", 256, "change to keyboard mode and set keycod")
flag.Parse()
if *debugFlag {
device.SetLogLevel(device.LogDebug)
}
if *listFlag {
action = "list"
} else if *readFlag {
action = "read"
}
log.Printf("beeon-cli core:%s usb:%t", device.Version(), device.UsbSupported())
if serialNumber != "" {
log.Printf("serial:%s", serialNumber)
}
if action == "list" {
devs, err := device.Enumerate(vendorID, productID)
if err != nil {
panic(err)
}
for i, _dev := range devs {
log.Printf("[%d] %s:v%s:%s:%s",
i,
_dev.Serial,
fmt.Sprintf("%d.%02d", (_dev.Version&0xff00)>>8, (_dev.Version&0x00ff)),
_dev.Manufacturer,
_dev.Product,
)
}
os.Exit(0)
}
if action == "read" {
r3, err := device.ReadReport3(vendorID, productID, serialNumber)
if err != nil {
log.Printf(err.Error())
os.Exit(1)
}
log.Printf("%+v", r3)
}
if inMouseMode != "" {
switch strings.ToLower(inMouseMode) {
case "circle":
newMouseMode = report3.MouseModeCircle
newMode = report3.ModeMouse
action = "write"
case "random":
newMouseMode = report3.MouseModeRandom
newMode = report3.ModeMouse
action = "write"
default:
log.Printf("invalid mouse-mode %s", inMouseMode)
}
}
if inKeyCode != 256 && action == "none" {
newKeyCode = keycodes.KeyCode(inKeyCode)
newMode = report3.ModeKeyboard
action = "write"
}
if inState != "" {
switch strings.ToLower(inState) {
case "idle":
newState = report3.StateIdle
action = "write"
case "active":
newState = report3.StateActive
action = "write"
default:
log.Printf("invalid state %s", inState)
}
}
if inRate != "" {
switch strings.ToLower(inRate) {
case "extra-slow":
newRate = report3.RateExtraSlow
action = "write"
case "slow":
newRate = report3.RateSlow
action = "write"
case "normal":
newRate = report3.RateNormal
action = "write"
case "fast":
newRate = report3.RateFast
action = "write"
default:
log.Printf("invalid rate %s", inRate)
}
}
if action == "none" {
flag.PrintDefaults()
os.Exit(1)
}
if action == "write" {
hasChange := false
whatChanged := []string{}
r3, err := device.ReadReport3(vendorID, productID, serialNumber)
if err != nil {
log.Printf(err.Error())
os.Exit(1)
}
if (newMode != report3.ModeUnset) && (r3.Mode != newMode) {
whatChanged = append(whatChanged, fmt.Sprintf("Mode(%s>%s)", r3.Mode, newMode))
r3.Mode = newMode
hasChange = true
} else {
r3.Mode = report3.ModeUnset
}
if newKeyCode != keycodes.KeyUnset && (newKeyCode != r3.KeyCode) {
whatChanged = append(whatChanged, fmt.Sprintf("KeyCode(%s>%s)", r3.KeyCode, newKeyCode))
r3.KeyCode = newKeyCode
hasChange = true
} else {
r3.KeyCode = keycodes.KeyUnset
}
if newMouseMode != report3.MouseModeUnset && (newMouseMode != r3.MouseMode) {
whatChanged = append(whatChanged, fmt.Sprintf("MouseMode(%s>%s)", r3.MouseMode, newMouseMode))
r3.MouseMode = newMouseMode
hasChange = true
} else {
r3.MouseMode = report3.MouseModeUnset
}
if newState != report3.StateUnset && (newState != r3.State) {
whatChanged = append(whatChanged, fmt.Sprintf("State(%s>%s)", r3.State, newState))
r3.State = newState
hasChange = true
} else {
r3.State = report3.StateUnset
}
if newRate != report3.RateUnset && (newRate != r3.Rate) {
whatChanged = append(whatChanged, fmt.Sprintf("Rate(%s>%s)", r3.Rate, newRate))
r3.Rate = newRate
hasChange = true
} else {
r3.Rate = report3.RateUnset
}
if hasChange {
err := device.WriteReport3(vendorID, productID, serialNumber, r3.Bytes())
if err != nil {
log.Printf(err.Error())
os.Exit(1)
}
log.Printf("changes made {%s}", strings.Join(whatChanged, "; "))
} else {
log.Printf("no changes")
}
}
}

5
app/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module microjelly.com/beeon/app
go 1.13
require github.com/microjelly/hid v0.0.0-20200814141453-3602713b3885

2
app/go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/microjelly/hid v0.0.0-20200814141453-3602713b3885 h1:UdpWecEd/pxs7YgX+gYnROdB4ID6oKIuv2GnVhhXofk=
github.com/microjelly/hid v0.0.0-20200814141453-3602713b3885/go.mod h1:bPSrfoMz72p90YvMJSC/hCd0xL7MebXTyM8oWhtsgu8=

106
app/pkg/device/device.go Normal file
View File

@@ -0,0 +1,106 @@
package device
import (
"errors"
"log"
"github.com/microjelly/hid"
"microjelly.com/beeon/app/pkg/device/report3"
)
var (
_devices []Info
_logLevel LogLevel = LogNone
)
// Version - returns the version of this module
func Version() string {
return "v1.0.1"
}
// UsbSupported - returns true if usb is supported on this platform
func UsbSupported() bool {
return hid.Supported()
}
// SetLogLevel - sets the logging level
func SetLogLevel(level LogLevel) {
_logLevel = level
}
// Enumerate - gets connected devices
func Enumerate(vendorID uint16, productID uint16) ([]Info, error) {
var (
_devices []Info = nil
pSN string = ""
)
for _, dev := range hid.Enumerate(vendorID, productID) {
if dev.Serial != pSN {
_devices = append(_devices, Info{
Serial: dev.Serial,
Version: dev.Release,
Manufacturer: dev.Manufacturer,
Product: dev.Product,
})
pSN = dev.Serial
}
}
return _devices, nil
}
func open(vendorID uint16, productID uint16, serial string) (*hid.Device, error) {
devices := hid.Enumerate(vendorID, productID)
for _, dev := range devices {
if (serial != "" && dev.Serial == serial) || serial == "" {
_log(LogInfo, "opening device [%s]", dev.Serial)
return dev.Open()
}
}
return nil, errors.New("device not found")
}
// ReadReport3 - get device configuration
func ReadReport3(vendorID uint16, productID uint16, serial string) (*report3.Report, error) {
report := []byte{0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
dev, err := open(vendorID, productID, serial)
if err != nil {
return nil, err
}
defer dev.Close()
_, err = dev.GetFeatureReport(report)
if err != nil {
return nil, err
}
r := report3.New(report)
_log(LogDebug, "read %v > %v", report, r)
return r, nil
}
// WriteReport3 - get device configuration
func WriteReport3(vendorID uint16, productID uint16, serial string, report []byte) error {
dev, err := open(vendorID, productID, serial)
if err != nil {
return err
}
defer dev.Close()
_log(LogDebug, "write %v > %v", report, report3.New(report))
_, err = dev.SendFeatureReport(report)
if err != nil {
return err
}
return nil
}
func _log(level LogLevel, format string, v ...interface{}) {
if level >= _logLevel {
log.Printf(format, v...)
}
}

View File

@@ -0,0 +1,175 @@
// The contents of this file is free and unencumbered software released into
// the public domain. Refer to <http://unlicense.org/> for more information.
// Src: https://github.com/yakshaveinc/go-keycodes/blob/master/keycodes.go
package keycodes
import "fmt"
type KeyCode uint8
func (k KeyCode) String() string {
if k == KeyUnset {
return "unset"
}
return fmt.Sprintf("Key(%d)", k)
}
// note: similar keys have different key codes, like Enter and
// Keypad Enter
// cross-platform key codes, compatible with SDL 2 and USB HID speccy
// https://hg.libsdl.org/SDL/file/default/include/SDL_scancode.h
// http://www.usb.org/developers/hidpage/Hut1_12v2.pdf (page 53)
// key for codes 0 - 3 are not present on keyboards, they are:
// 0 - Reserved (no event)
// 1 - ErrorRollOver
// 2 - POSTFail
// 3 - ErrorUndefined
const (
KeyA KeyCode = 4 + iota
KeyB
KeyC
KeyD
KeyE
KeyF
KeyG
KeyH
KeyI
KeyJ
KeyK
KeyL
KeyM
KeyN
KeyO
KeyP
KeyQ
KeyR
KeyS
KeyT
KeyU
KeyV
KeyW
KeyX
KeyY
KeyZ
)
const (
Key1 KeyCode = 30 + iota
Key2
Key3
Key4
Key5
Key6
Key7
Key8
Key9
Key0
)
const (
// choice is to use Enter name instead of Return
// and the key code is different from Keypad Enter
KeyEnter KeyCode = 40 + iota
KeyEscape
KeyBackspace
KeyTab
KeySpace
// keypad minus has different key code
KeyMinus
KeyEquals
KeyLeftBracket
KeyRightBracket
KeyBackslash
)
// key code number 50 is skipped, because it is unclear
// where is the key, and what is its name and function
const (
// different name from SDL2 for brevity
KeyColon KeyCode = 51 + iota
KeyApostrophe
// KeyTilde is an alias
KeyGrave
KeyCommad
// KeyDot is an alias, keypad period is a different
KeyPeriod
Slash
CapsLock
)
const KeyTilde = KeyGrave
const KeyDot = KeyPeriod
const (
KeyF1 KeyCode = 58 + iota
KeyF2
KeyF3
KeyF4
KeyF5
KeyF6
KeyF7
KeyF8
KeyF9
KeyF10
KeyF11
KeyF12
)
const (
KeyPrintScreen KeyCode = 70 + iota
KeyScrollLock
KeyPause
KeyInsert
KeyHome
KeyPageUp
KeyDelete
KeyEnd
KeyPageDown
KeyRight
KeyLeft
KeyDown
KeyUp
)
const (
KeyNumLock KeyCode = 83 + iota
KeyKpDivide
KeyKpMultiply
KeyKpMinus
KeyKpPlus
KeyKpEnter
KeyKp1
KeyKp2
KeyKp3
KeyKp4
KeyKp5
KeyKp6
KeyKp7
KeyKp8
KeyKp9
KeyKp0
// KeyKpDot is an alias
KeyKpPeriod
)
const KeyKpDot = KeyKpPeriod
// key code 100 is skipped, because I can not find the key
// key code 101 is not present on Mac
// key codes 102-223 are not present on PC
const (
KeyLCtrl KeyCode = 224 + iota
KeyLShift
KeyLAlt
// KeyLWin is an alias
KeyLGUI
KeyRCtrl
KeyRShift
KeyRAlt
// KeyRWin is an alias
KeyRGUI
)
const KeyLWin = KeyLGUI
const KeyRWin = KeyRGUI
const KeyUnset = 255

View File

@@ -0,0 +1,139 @@
package report3
import (
"fmt"
"microjelly.com/beeon/app/pkg/device/keycodes"
)
type State uint8
const (
StateIdle = State(10)
StateActive = State(20)
StateUnset = State(255)
)
func (r State) String() string {
switch r {
case StateActive:
return "active"
case StateIdle:
return "idle"
case StateUnset:
return "unset"
default:
return "unknown"
}
}
type Mode uint8
const (
ModeMouse = Mode(10)
ModeKeyboard = Mode(20)
ModeUnset = Mode(255)
)
func (r Mode) String() string {
switch r {
case ModeMouse:
return "mouse"
case ModeKeyboard:
return "keyboard"
case ModeUnset:
return "unset"
default:
return "unknown"
}
}
type Rate uint8
const (
RateFast = Rate(1)
RateNormal = Rate(8)
RateSlow = Rate(16)
RateExtraSlow = Rate(32)
RateUnset = Rate(255)
)
func (r Rate) String() string {
switch r {
case RateFast:
return "fast"
case RateNormal:
return "normal"
case RateSlow:
return "slow"
case RateExtraSlow:
return "extra-slow"
case RateUnset:
return "unset"
default:
return fmt.Sprintf("custom (%d)", r)
}
}
type MouseMode uint8
const (
MouseModeCircle = MouseMode(10)
MouseModeRandom = MouseMode(20)
MouseModeUnset = MouseMode(255)
)
func (r MouseMode) String() string {
switch r {
case MouseModeCircle:
return "circle"
case MouseModeRandom:
return "random"
case MouseModeUnset:
return "unset"
default:
return "unknown"
}
}
type Report struct {
State State
Mode Mode
Rate Rate
KeyCode keycodes.KeyCode
MouseMode MouseMode
}
func (r Report) String() string {
switch r.Mode {
case ModeMouse:
return fmt.Sprintf("{Mode: %s, MouseMode: %s, Rate: %s, State: %s}", r.Mode, r.MouseMode, r.Rate, r.State)
case ModeKeyboard:
return fmt.Sprintf("{Mode: %s, KeyCode: %s, Rate: %s, State: %s}", r.Mode, r.KeyCode, r.Rate, r.State)
case ModeUnset:
return fmt.Sprintf("{Mode: %s, MouseMode: %s, KeyCode: %s, Rate: %s, State: %s}", r.Mode, r.MouseMode, r.KeyCode, r.Rate, r.State)
default:
return "{invalid}"
}
}
func New(b []byte) *Report {
r := new(Report)
r.State = State(b[1])
r.Mode = Mode(b[2])
r.Rate = Rate(b[3])
r.KeyCode = keycodes.KeyCode(b[4])
r.MouseMode = MouseMode(b[5])
return r
}
func (r Report) Bytes() []byte {
b := make([]byte, 6)
b[0] = 3
b[1] = uint8(r.State)
b[2] = uint8(r.Mode)
b[3] = uint8(r.Rate)
b[4] = uint8(r.KeyCode)
b[5] = uint8(r.MouseMode)
return b
}

21
app/pkg/device/types.go Normal file
View File

@@ -0,0 +1,21 @@
package device
// Info - short list of details
type Info struct {
Serial string
Manufacturer string
Version uint16
Product string
}
// LogLevel - something
type LogLevel uint
// LogNone - disabled logging
// LogInfo - basic logging
// LogDebug - even more
const (
LogDebug LogLevel = iota
LogInfo
LogNone
)