Logitech Craft Keyboard (hardware)
https://www.logitech.com/product/craft
Logitech Options 6.80 and above (software)
http://support.logitech.com/software/options
Install wxPython
from the command line:
pip3 install wxpython
Install websocket-client
from command line:
pip3 install websocket-client
Install pyinstaller
from the command line:
pip3 install pyinstaller
You will need to use pyinstaller
to create a Windows executable or a macOS bundle.
Windows:
pyinstaller craft.py
macOS:
Convert craft.py
to craft.app
using the pyinstaller
tool:
pyinstaller -windowed craft.py
Windows:
Create a folder in Windows as shown in the figure. (ProgramData/Logishrd/LogiOptionsPlugins)
.
Copy the sample manifest folder 6202f2fb-834c-4393-a95f-f5051171e3ec
into the LogiOptionsPlugins
folder.
macOS:
Open the file 6202f2fb-834c-4393-a95f-f5051171e3ec/Manifest/defaults.json
from your text editor and:
-
replace the string
<-- full path of the folder that contains the craft.app bundle here -->
with the folder path of./dist
. -
replace the string
<-- full path of craft.app bundle here -->
with the full path of./dist/craft.app
.
Create the folder ~/Library/Application\ Support/Logitech/Logitech\ Options/Plugins
and copy the sample manifest folder 6202f2fb-834c-4393-a95f-f5051171e3ec
into the Plugins
folder.
Windows:
Run craft.exe
from the command line.
macOS:
Run craft.app
from Finder
, or enter open dist/craft.app
from the command line.
Start Logitech Options and click Craft Advanced Keyboard.
Click MORE SETTINGS
.
Click ENABLE
button from the Developer Mode section.
Click All Applications
in the top of the panel and scroll down to Add application
. You should see a round icon with the number of applications with a supported plugin.
Click All Applications
and click round icon next to Add application
.
Click INSTALL PROFILES
to install applications to the Logitech Options.
Application added to the list for installation and click INSTALL PROFILES
.
Click CONTINUE
button.
Applications added to the Logitech Options.
By starting the Craft application, user can click the controls to get the context and then able to turn the Crown in the Craft for interaction with various controls in the application.
Contact Logitech at craftSDK[at]logitech[dot]com
for signing and whitelisting of your application plugin so that it works even in non-developer mode.
Below steps are for new manifest file creation
Create a GUID (Globally Unique Identifier) using an online GUID generator. Create a folder in the same name as the GUID as shown below. And create 3 folders (Gallery, Languages and Manifest)
Create 2 files in the Manifest folder (defaults.json
, tools.json
)
defaults.json
{
"GUID": "6202f2fb-834c-4393-a95f-f5051171e3ec",
"info": {
"name": "Craft Python SDK Sample",
"publisher": "Logitech Inc.",
"version": "1.0",
"win_name": "craft.exe",
"win_minimum_supported_version": "0.0.0",
"win_maximum_supported_version": "2017.0.1",
"mac_bundle": "craft.app",
"mac_path": "<-- folder path of craft.app here -->",
"mac_paths": [
{
"path": "<-- full path of craft.app here -->",
"mac_minimum_supported_version": "0.0.0",
"mac_maximum_supported_version": "10.0.0",
"name_suffix": ""
}
]
},
"crown": {
"rotate": {
"default_task": "changetoolvalue",
"tasks": [
"changetoolvalue"
],
"short_list": [
"changetoolvalue"
]
},
"press": {
"default_task": "playpause",
"tasks": [
"playpause"
],
"short_list": [
"playpause"
]
}
}
}
Change the GUID key to the online generated value. Change the name, publisher, version and win_name as shown in the figure.
tools.json
{
"GUID": "6202f2fb-834c-4393-a95f-f5051171e3ec",
"tools": [
{
"name": "Slider",
"enabled": true,
"tool_options": [
{
"index": 0,
"name": "slider",
"image_file_path": "horizontal.png",
"enabled": true,
"ratchet_enabled": false
}
]
},
{
"name": "SpinCtrl",
"enabled": true,
"tool_options": [
{
"index": 0,
"name": "spinCtrl",
"image_file_path": "numericUpDown.png",
"enabled": true,
"ratchet_enabled": false
}
]
},
{
"name": "Gauge",
"enabled": true,
"tool_options": [
{
"index": 0,
"name": "gauge",
"image_file_path": "progressBar.png",
"enabled": true,
"ratchet_enabled": false
},
{
"index": 1,
"name": "gaugeRatchet",
"image_file_path": "progressBar.png",
"enabled": true,
"ratchet_enabled": true
}
]
}
.
.
.
]
}
Create a tools.json
file and add the GUID in the top of the file as shown above. Add other information and name is the name of the control that Craft need to control. image_file_path
is the image file that is shown in the overlay. ratchet_enabled controls the ratchet or freewheel mode.
Create a file called en.json
for English version. The LocalizedStrings contain the ID and value key. ID corresponds to the name in the tool_options in the tools.json
.
{
"LocalizedStrings": [
{
"ID": "slider",
"value": "Slider"
},
{
"ID": "spinCtrl",
"value": "SpinCtrl"
},
{
"ID": "gauge",
"value": "Gauge"
},
{
"ID": "textCtrl",
"value": "TextCtrl"
}
.
.
.
Create a Gallery folder and copy all the image files that are referenced in the tools.json
.
Application connect with the Craft on port 10134 using websocket. Then on_open gets called, which register with the Craft.
def connect(self, execName,manifestFilePath):
print("connect called...")
global ws
self.executableName = execName
self.manifestPath = manifestFilePath
websocket.enableTrace(True)
ws = websocket.WebSocketApp("ws://127.0.0.1:10134",
on_open = self.on_open,
on_message = self.on_message,
on_close = self.on_close)
wst = threading.Thread(target=ws.run_forever)
wst.daemon = True
wst.start()
def on_open(self,ws):
print("on_open called...")
uid = "6202f2fb-834c-4393-a95f-f5051171e3ec"
pid = os.getpid()
connectMessage = {
"message_type": "register",
"plugin_guid": uid,
"PID": pid,
"execName": self.executableName,
"manifestPath": self.manifestPath,
"application_version": "0.0.0"
}
regMsg = json.dumps(connectMessage)
ws.send(regMsg.encode('utf8'))
Craft turn and touch messages are collected in the on_message routine.
def on_message(self,ws, message):
print("on_message called...")
# craft events come in as json objects
craftEventObj = json.loads(message)
wx.CallAfter(self.wrapperUpdateUI, craftEventObj)
wxPython
GUI toolkit was used in this application
def __init__(self, parent, id):
global craft,slider,spin,gauge,combo,txt,lb
wx.Frame.__init__(self, parent, id, "Craft Python SDK Sample", size=(800,400))
panel = wx.Panel(self)
lbl = wx.StaticText(panel,-1, label="text", pos=(10,20), size=(50,-1))
lbl.SetLabel("Slider")
slider=wx.Slider(panel, -1, 0, 1, 1000, (100,20), (200,-1))
slider.Bind(wx.EVT_SET_FOCUS, self.sliderFocus)
slider.Bind(wx.EVT_LEFT_UP, self.sliderFocus)
.
.
.
lbl = wx.StaticText(panel, -1, label="text", pos=(400,180), size=(50,-1))
lbl.SetLabel("ListBox")
li =[]
for i in range(0, 1000):
li.append(str(i))
lb = wx.ListBox(panel, -1, pos=(480,180), size=(100,-1), choices=li)
lb.Bind(wx.EVT_SET_FOCUS, self.listBoxFocus)
lb.Bind(wx.EVT_LEFT_UP, self.listBoxFocus)
TextFrame
was created using the wx.Frame
function and panel object was created by passing the Frame object as a parent in the wx.Panel
argument. All other components are added to the panel object. For example, StaticText
and Slider
controls were added to the panel.
li =[]
for i in range(0, 1000):
li.append(str(i))
lb = wx.ListBox(panel, -1, pos=(480,180), size=(100,-1), choices=li)
lb.Bind(wx.EVT_SET_FOCUS, self.listBoxFocus)
lb.Bind(wx.EVT_LEFT_UP, self.listBoxFocus)
ListBox
is added to the panel by passing panel object as the parent, position, size and choices. Choices are the list of objects to be added to the listbox.
def listBoxFocus(self, event):
print("ListBox receives focus")
self.changeTool("ListBox")
event.Skip()
def changeTool(self, name):
connectMessage = {
"message_type": "tool_change",
"session_id": sessionId,
"tool_id": name
}
regMsg = json.dumps(connectMessage)
ws.send(regMsg.encode('utf8'))
All controls are attached to the event using the Bind function. Mouse clicking or setting focus on the ListBox
will call the listBoxFocus
function. The listBoxFocus
function creates a connectMessage
json object with the following parameters message_type
, session_id
, and tool_id
and sends this json object to the Craft using the websocket send command.
if(msg['message_type'] == "crown_turn_event"):
glist.append(msg)
listCount = len(glist)
if listCount==0:
return
currentToolOption = glist[0]['task_options']['current_tool_option']
print("+++currentToolOption = ",currentToolOption)
print("listCount = ",listCount)
firstObject = glist[0]
for i in range(listCount):
if currentToolOption == glist[i]['task_options']['current_tool_option']:
totalDeltaValue = totalDeltaValue = glist[i]['delta']
totalRatchetDeltaValue = totalRatchetDeltaValue + glist[i]['ratchet_delta']
else:
break
count += 1
if listCount >= 0:
glist.clear()
print("totalDeltaValue = ",totalDeltaValue)
print("firstObject = ",firstObject['message_type'])
if firstObject['message_type'] == "deactivate_plugin":
return
All turn event of the Craft are added to the glist and total delta value is calculated as shown in the above figure.
try:
if firstObject['message_type'] == "crown_turn_event":
print("turn event =====")
if firstObject['task_options']['current_tool'] == 'Slider':
print("\n","selected slider")
v = slider.GetValue()
tvalue = v + totalDeltaValue
slider.SetValue(tvalue)
elif firstObject['task_options']['current_tool'] == 'SpinCtrl':
print("\n","selected SpinCtrl")
v = spin.GetValue()
tvalue = v + totalDeltaValue
spin.SetValue(tvalue)
elif firstObject['task_options']['current_tool'] == 'Gauge':
print("\n","selected Gauge")
v = gauge.GetValue()
tvalue = v + totalDeltaValue
gauge.SetValue(tvalue)
If the Craft message type is crown_turn_event
then delta value is applied for that tool.
if __name__ == '__main__':
global ws
global craft
app = wx.App()
frame = TestFrame(parent=None, id=-1)
frame.Show()
craft = CraftClient()
if platform.system() == 'Windows':
craft.connect("Craft.exe", "")
else:
craft.connect("craft.app", "")
app.MainLoop()
Above code starts the application by creating the wx.App()
object of wxPython
for GUI and CraftClient which interact with the Craft keyboard.