Files
agent/packages/usb/uhk.js
2019-05-15 23:35:05 +02:00

563 lines
17 KiB
JavaScript

const HID = require('node-hid');
const {HardwareConfiguration, UhkBuffer} = require('uhk-common');
const Logger = require('./logger');
const debug = process.env.DEBUG;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const kbootCommandIdToName = {
0: 'idle',
1: 'ping',
2: 'reset',
};
const eepromOperationIdToName = {
0: 'read',
1: 'write',
};
function bufferToString(buffer) {
let str = '';
for (let i = 0; i < buffer.length; i++) {
let hex = buffer[i].toString(16) + ' ';
if (hex.length <= 2) {
hex = '0' + hex;
}
str += hex;
}
return str;
}
function getUint16(buffer, offset) {
return (buffer[offset]) + (buffer[offset+1] * 2**8);
}
function getUint32(buffer, offset) {
return (buffer[offset]) + (buffer[offset+1] * 2**8) + (buffer[offset+2] * 2**16) + (buffer[offset+3] * 2**24);
}
function uint32ToArray(value) {
return [(value >> 0) & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff];
}
function writeDevice(device, data, options={}) {
const dataBuffer = new Buffer(data);
if (!options.noDebug) {
writeLog('W: ', dataBuffer);
}
device.write(getTransferData(dataBuffer));
if (options.noRead) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
device.read((err, data) => {
if (err) {
reject(err);
} else {
if (!options.noDebug) {
writeLog('R: ', data);
}
resolve(data);
}
});
});
}
function getUhkDevice() {
const foundDevice = HID.devices().find(device =>
device.vendorId === 0x1d50 && device.productId === 0x6122 &&
// hidapi can not read the interface number on Mac, so check the usage page and usage
((device.usagePage === 128 && device.usage === 129) || // Old firmware
(device.usagePage === (0xFF00 | 0x00) && device.usage === 0x01) || // New firmware
device.interface === 0));
if (!foundDevice) {
return null;
}
let hid;
try {
hid = new HID.HID(foundDevice.path);
} catch (error) {
// Already jumped to the bootloader, so ignore this exception.
return null;
}
return hid;
}
function getBootloaderDevice() {
const foundDevice = HID.devices().find(device =>
device.vendorId === 0x1d50 && device.productId === 0x6120);
if (!foundDevice) {
return null;
}
return foundDevice;
}
function checkFirmwareImage(imagePath, extension) {
if (!imagePath) {
echo('No firmware image specified.');
exit(1);
}
if (!imagePath.endsWith(extension)) {
echo(`Firmware image extension is not ${extension}`);
exit(1);
}
if (!test('-f', imagePath)) {
echo('Firmware image does not exist.');
exit(1);
}
}
function getBlhostCmd(pid) {
let blhostPath;
switch (process.platform) {
case 'linux':
const arch = exec('uname -m', {silent:true}).stdout.trim();
blhostPath = `linux/${arch}/blhost`;
break;
case 'darwin':
blhostPath = 'mac/blhost';
break;
case 'win32':
blhostPath = 'win/blhost.exe';
break;
default:
echo('Your operating system is not supported.');
exit(1);
break;
}
return `${__dirname}/blhost/${blhostPath} --usb 0x1d50,0x${pid.toString(16)}`;
}
function execRetry(command) {
let firstRun = true;
let remainingRetries = 3;
let code;
do {
if (!firstRun) {
console.log(`Retrying ${command}`)
}
config.fatal = !remainingRetries;
code = exec(command).code;
config.fatal = true;
firstRun = false;
} while(code && --remainingRetries);
}
const configBufferIds = {
hardwareConfig: 0,
stagingUserConfig: 1,
validatedUserConfig: 2,
};
const configBufferIdToName = {
0: 'hardwareConfig',
1: 'stagingUserConfig',
2: 'validatedUserConfig',
}
let eepromOperations = {
read: 0,
write: 1,
};
async function updateDeviceFirmware(firmwareImage, extension) {
const usbDir = `${__dirname}`;
const blhost = uhk.getBlhostCmd(uhk.enumerationNameToProductId.bootloader);
uhk.checkFirmwareImage(firmwareImage, extension);
config.verbose = true;
await uhk.reenumerate('bootloader');
exec(`${blhost} flash-security-disable 0403020108070605`);
exec(`${blhost} flash-erase-region 0xc000 475136`);
exec(`${blhost} flash-image ${firmwareImage}`);
exec(`${blhost} reset`);
config.verbose = false;
echo('Firmware updated successfully.');
};
// USB commands
function reenumerate(enumerationMode, bootloaderTimeoutMs=5000) {
const pollingIntervalMs = 100;
let pollingTimeoutMs = 10000;
let jumped = false;
return new Promise((resolve, reject) => {
const enumerationModeId = exports.enumerationModes[enumerationMode];
if (enumerationModeId === undefined) {
const enumerationModes = Object.keys(exports.enumerationModes).join(', ');
console.log(`Invalid enumeration mode '${enumerationMode}' is not one of: ${enumerationModes}`);
reject();
return;
}
console.log(`Trying to reenumerate as ${enumerationMode}...`);
const intervalId = setInterval(async function() {
pollingTimeoutMs -= pollingIntervalMs;
const foundDevice = HID.devices().find(device =>
device.vendorId === exports.vendorId && device.productId === exports.enumerationModeIdToProductId[enumerationModeId]);
if (foundDevice) {
console.log(`${enumerationMode} is up`);
resolve();
clearInterval(intervalId);
return;
}
if (pollingTimeoutMs <= 0) {
console.log(`Couldn't reenumerate as ${enumerationMode}`);
reject();
clearInterval(intervalId);
return;
}
let device = exports.getUhkDevice();
if (device && !jumped) {
console.log(`UHK found, reenumerating as ${enumerationMode}`);
await writeDevice(device, [exports.usbCommands.reenumerate, enumerationModeId, ...uint32ToArray(bootloaderTimeoutMs)], {noRead:true});
jumped = true;
}
}, pollingIntervalMs);
})
};
async function sendKbootCommandToModule(device, kbootCommandId, i2cAddress) {
writeLog(`T: sendKbootCommandToModule kbootCommandId:${kbootCommandIdToName[kbootCommandId]} i2cAddress:${i2cAddress}`);
return await uhk.writeDevice(device, [uhk.usbCommands.sendKbootCommandToModule, kbootCommandId, parseInt(i2cAddress)])
};
async function jumpToModuleBootloader(device, moduleSlotId) {
writeLog(`T: jumpToModuleBootloader moduleSlotId:${moduleSlotId}`);
await uhk.writeDevice(device, [uhk.usbCommands.jumpToModuleBootloader, moduleSlotId]);
};
async function switchKeymap(device, keymapAbbreviation) {
writeLog(`T: switchKeymap keymapAbbreviation:${keymapAbbreviation}`);
const keymapAbbreviationAscii = keymapAbbreviation.split('').map(char => char.charCodeAt(0));
const payload = [uhk.usbCommands.switchKeymap, keymapAbbreviation.length, ...keymapAbbreviationAscii];
return await uhk.writeDevice(device, payload);
}
async function waitForKbootIdle(device) {
writeLog(`T: waitForKbootIdle`);
const intervalMs = 100;
const pingMessageInterval = 500;
let timeoutMs = 10000;
return new Promise((resolve, reject) => {
const intervalId = setInterval(async function() {
const response = await uhk.writeDevice(device, [uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.currentKbootCommand]);
const currentKbootCommand = response[1];
if (currentKbootCommand == 0) {
console.log('Bootloader pinged.');
resolve();
clearInterval(intervalId);
return;
} else if (timeoutMs % pingMessageInterval === 0) {
console.log("Cannot ping the bootloader. Please reconnect the left keyboard half. It probably needs several tries, so keep reconnecting until you see this message.");
};
timeoutMs -= intervalMs;
if (timeoutMs < 0) {
reject();
clearInterval(intervalId);
return;
}
}, intervalMs);
});
}
async function updateModuleFirmware(i2cAddress, moduleSlotId, firmwareImage) {
const usbDir = `${__dirname}`;
const blhostUsb = uhk.getBlhostCmd(uhk.enumerationNameToProductId.buspal);
const blhostBuspal = `${blhostUsb} --buspal i2c,${i2cAddress}`;
config.verbose = true;
let device = uhk.getUhkDevice();
await uhk.sendKbootCommandToModule(device, uhk.kbootCommands.ping, i2cAddress);
await uhk.jumpToModuleBootloader(device, moduleSlotId);
await uhk.waitForKbootIdle(device);
device.close();
await uhk.reenumerate('buspal');
uhk.execRetry(`${blhostBuspal} get-property 1`);
exec(`${blhostBuspal} flash-erase-all-unsecure`);
exec(`${blhostBuspal} write-memory 0x0 ${firmwareImage}`);
exec(`${blhostUsb} reset`);
await uhk.reenumerate('normalKeyboard');
device = uhk.getUhkDevice();
await uhk.sendKbootCommandToModule(device, uhk.kbootCommands.reset, i2cAddress);
await sleep(1000);
await uhk.sendKbootCommandToModule(device, uhk.kbootCommands.idle, i2cAddress);
device.close();
config.verbose = false;
echo('Firmware updated successfully.');
};
async function updateFirmwares(firmwarePath) {
console.log('Updating right firmware');
await uhk.updateDeviceFirmware(`${firmwarePath}/devices/uhk60-right/firmware.hex`, 'hex');
await uhk.reenumerate('normalKeyboard');
console.log('Updating left firmware');
await uhk.updateModuleFirmware(uhk.moduleSlotToI2cAddress.leftHalf, uhk.moduleSlotToId.leftHalf, `${firmwarePath}/modules/uhk60-left.bin`);
}
async function writeConfig(device, configBuffer, isHardwareConfig) {
writeLog(`T: writeConfig isHardwareConfig:${isHardwareConfig}`);
const chunkSize = 60;
let offset = 0;
let chunkSizeToRead;
let buffer = await uhk.writeDevice(device, [uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.configSizes]);
const hardwareConfigMaxSize = getUint16(buffer, 1);
const userConfigMaxSize = getUint16(buffer, 3);
const configMaxSize = isHardwareConfig ? hardwareConfigMaxSize : userConfigMaxSize;
const configSize = Math.min(configMaxSize, configBuffer.length);
writeLog('WR: ...');
while (offset < configSize) {
const usbCommand = isHardwareConfig ? uhk.usbCommands.writeHardwareConfig : uhk.usbCommands.writeStagingUserConfig;
chunkSizeToRead = Math.min(chunkSize, configSize - offset);
if (chunkSizeToRead === 0) {
break;
}
buffer = [
usbCommand, chunkSizeToRead, offset & 0xff, offset >> 8,
...configBuffer.slice(offset, offset+chunkSizeToRead)
];
await uhk.writeDevice(device, buffer, {noDebug:true})
offset += chunkSizeToRead;
}
}
async function applyConfig(device) {
writeLog(`T: applyConfig`);
await uhk.writeDevice(device, [uhk.usbCommands.applyConfig]);
}
async function launchEepromTransfer(device, operation, configBufferId) {
writeLog(`T: launchEepromTransfer operation:${eepromOperationIdToName[operation]}`);
const buffer = await uhk.writeDevice(device, [uhk.usbCommands.launchEepromTransfer, operation, configBufferId]);
isBusy = true;
writeLog(`T: getDeviceState`);
do {
const buffer = await uhk.writeDevice(device, [uhk.usbCommands.getDeviceState]);
isBusy = buffer[1] === 1;
} while (isBusy);
};
async function writeUca(device, configBuffer) {
await uhk.writeConfig(device, configBuffer, false);
await uhk.applyConfig(device);
await uhk.launchEepromTransfer(device, uhk.eepromOperations.write, configBufferIds.validatedUserConfig);
}
async function writeHca(device, isIso) {
const hardwareConfig = new HardwareConfiguration();
hardwareConfig.signature = 'UHK';
hardwareConfig.majorVersion = 1;
hardwareConfig.minorVersion = 0;
hardwareConfig.patchVersion = 0;
hardwareConfig.brandId = 0;
hardwareConfig.deviceId = 1;
hardwareConfig.uniqueId = Math.floor(2**32 * Math.random());
hardwareConfig.isVendorModeOn = false;
hardwareConfig.isIso = isIso;
const logger = new Logger();
const hardwareBuffer = new UhkBuffer();
hardwareConfig.toBinary(hardwareBuffer);
const buffer = hardwareBuffer.getBufferContent();
await uhk.writeConfig(device, buffer, true);
await uhk.launchEepromTransfer(device, uhk.eepromOperations.write, configBufferIds.hardwareConfig);
}
async function eraseHca(device) {
const buffer = new Buffer(Array(64).fill(0xff));
await uhk.writeConfig(device, buffer, true);
await uhk.launchEepromTransfer(device, uhk.eepromOperations.write, configBufferIds.hardwareConfig);
}
async function getModuleProperty(device, slotId, moduleProperty) {
await writeDevice(device, [uhk.usbCommands.getModuleProperty, slotId, moduleProperty]);
}
uhk = exports = module.exports = moduleExports = {
bufferToString,
getUint16,
getUint32,
uint32ToArray,
writeDevice,
getUhkDevice,
getBootloaderDevice,
getTransferData,
checkModuleSlot,
checkFirmwareImage,
getBlhostCmd,
execRetry,
updateDeviceFirmware,
reenumerate,
sendKbootCommandToModule,
jumpToModuleBootloader,
switchKeymap,
waitForKbootIdle,
updateModuleFirmware,
updateFirmwares,
writeConfig,
applyConfig,
launchEepromTransfer,
writeUca,
writeHca,
eraseHca,
getModuleProperty,
usbCommands: {
getDeviceProperty : 0x00,
reenumerate : 0x01,
jumpToModuleBootloader : 0x02,
sendKbootCommandToModule: 0x03,
readConfig : 0x04,
writeHardwareConfig : 0x05,
writeStagingUserConfig : 0x06,
applyConfig : 0x07,
launchEepromTransfer : 0x08,
getDeviceState : 0x09,
setTestLed : 0x0a,
getDebugBuffer : 0x0b,
getAdcValue : 0x0c,
setLedPwmBrightness : 0x0d,
getModuleProperty : 0x0e,
getSlaveI2cErrors : 0x0f,
setI2cBaudRate : 0x10,
switchKeymap : 0x11,
getVariable : 0x12,
setVariable : 0x13,
},
enumerationModes: {
bootloader: 0,
buspal: 1,
normalKeyboard: 2,
compatibleKeyboard: 3,
},
enumerationModeIdToProductId: {
'0': 0x6120,
'1': 0x6121,
'2': 0x6122,
'3': 0x6123,
},
enumerationNameToProductId: {
bootloader: 0x6120,
buspal: 0x6121,
normalKeyboard: 0x6122,
compatibleKeyboard: 0x6123,
},
variableNameToId: {
testSwitches: 0,
testUsbStack: 1,
debounceTimePress: 2,
debounceTimeRelease: 3,
usbReportSemaphore: 4,
},
vendorId: 0x1D50,
devicePropertyIds: {
deviceProtocolVersion: 0,
protocolVersions: 1,
configSizes: 2,
currentKbootCommand: 3,
i2cBaudRate: 4,
uptime: 5,
},
modulePropertyIds: {
protocolVersions: 0,
},
configBufferIds,
eepromOperations,
kbootCommands: {
idle: 0,
ping: 1,
reset: 2,
},
moduleSlotToI2cAddress: {
leftHalf: '0x10',
leftModule: '0x20',
rightModule: '0x30',
},
moduleSlotToId: {
leftHalf: 1,
leftModule: 2,
rightModule: 3,
},
leftLedDriverAddress: 0b1110100,
rightLedDriverAddress: 0b1110111,
sendLog: sendLog,
readLog: readLog
};
function convertBufferToIntArray(buffer) {
return Array.prototype.slice.call(buffer, 0)
}
function getTransferData(buffer) {
// From HID API documentation:
// http://www.signal11.us/oss/hidapi/hidapi/doxygen/html/group__API.html#gad14ea48e440cf5066df87cc6488493af
// The first byte of data[] must contain the Report ID.
// For devices which only support a single report, this must be set to 0x0.
return [0, ...convertBufferToIntArray(buffer)];
}
function readLog(buffer) {
writeLog('USB[R]: ', buffer)
}
function sendLog(buffer) {
writeLog('USB[W]: ', buffer)
}
function writeLog(prefix, buffer) {
if (!debug) {
return;
}
if (buffer) {
console.log(prefix + bufferToString(buffer))
} else {
console.log(prefix);
}
}
function checkModuleSlot(moduleSlot, mapping) {
const mapped = mapping[moduleSlot];
if (moduleSlot == undefined) {
console.log(`No moduleSlot specified.`);
process.exit(1);
}
if (mapped == undefined) {
console.log(`Invalid moduleSlot "${moduleSlot}" specified.`);
console.log(`Valid module slots are: ${Object.keys(mapping).join(', ')}.`);
process.exit(1);
}
return mapped;
}