"use strict"; var p7 = {}; (function() { /* ---------------------Utilitaries--------------------- */ /* Convert a string into an array of ascii numbers */ String.prototype.toAscii = function () { return this.split('').map((char) => char.charCodeAt(0)) } /* Convert an hex number into an array of ascii values * Use the uppercase representation of the number */ Number.prototype.hexToAscii = function (padding) { return this.toString(16).toUpperCase().padStart(padding, 0).toAscii(); } /* Sum individual bytes * NOT + 1, as defined in fxReverse documentation. * Be sure it's under 256! */ Array.prototype.p7Checksum = function () { return (((~(this.reduce((accumulator, currentValue) => accumulator + currentValue))) + 1) & 0xFF) } /* Add the Checksum (CS) field to a packet */ Array.prototype.addP7Checksum = function () { return this.concat(this.slice(1, this.length).p7Checksum().hexToAscii(2)); } /* Convert an ascii array into a string */ Array.prototype.asciiToString = function () { let string = []; this.forEach((element) => string.push(String.fromCharCode(element))); return string.join(''); } /* Convert an ascii array representing an hex number into a number */ Array.prototype.asciiToHex = function () { return Number('0x' + this.asciiToString()); } /* Return the decoded Data (D) field */ Array.prototype.decodeDataField = function () { let buffer = [...this] buffer.forEach((element, index) => { if (element === 0x5C) buffer.splice(index, 2, buffer[index + 1] - (buffer[index + 1] !== 0x5C) * 0x20); }); return buffer; } /* Return the encoded Data (D) field */ Array.prototype.encodeDataField = function () { return this.flatMap((element) => (element < 0x20) ? [0x5C, (element + 0x20)] : (element === 0x5C) ? [0x5C, 0x5C] : element); } /* Return all the informations of the device, and trim string's overage (255) */ Array.prototype.extendedAckPacket = function () { return { hardwareIdentifier : this.slice(8, 16).asciiToString(), processorIdentifier : this.slice(16, 32).asciiToString(), preprogrammedROMCapacity : this.slice(32, 40).asciiToString(), flashROMCapacity : this.slice(40, 48).asciiToString(), ramCapacity : this.slice(48, 56).asciiToString(), prepogrammedROMVersion : this.slice(56, 72).asciiToString(), bootCodeVersion : this.slice(72, 88).asciiToString(), bootCodeOffset : this.slice(88, 96).asciiToString(), bootCodeSize : this.slice(96, 104).asciiToString(), osCodeVersion : this.slice(104, 120).filter(number => number !== 0xFF).asciiToString(), osCodeOffset : this.slice(120, 128).asciiToString(), osCodeSize : this.slice(128, 136).asciiToString(), protocolVersion : this.slice(136, 140).asciiToString(), productID : this.slice(140, 156).filter(number => number !== 0xFF).asciiToString(), username : this.slice(156, 172).filter(number => number !== 0xFF).asciiToString() } }; /* Return the value (as an number) of the Overwrite (OW) subfield of the Data (D) field of an extended command packet */ Array.prototype.commandPacketOverWrite = function () { return this.slice(8, 10).asciiToHex(); } /* Return the value (as an number) of the Data type (DT) subfield of the Data (D) field of an extended command packet */ Array.prototype.commandPacketDataType = function () { return this.slice(10, 12).asciiToHex(); } /* Return the value (as an number) of the FileSize (FS) subfield of the Data (D) field of an extended command packet */ Array.prototype.commandPacketFileSize = function () { return this.slice(12, 20).asciiToHex(); } /* Return the value (as a string) of the requested Data (D) subfield of the Data (D) field of an extended command packet */ Array.prototype.commandPacketDataField = function (field) { let fieldSize = []; /* Decode the length of Data {1 - ${length}} (D{1 - ${length}}) subfields */ for (let index = 20, i = 0; i < field ; i++) fieldSize.push(this.slice(index, (index += 2)).asciiToHex()); /* Get the requested field size */ let fieldLength = fieldSize.pop(); /* Get the index of the requested field */ let fieldIndex = (fieldSize.length) ? fieldSize.reduce((accumulator, currentValue) => accumulator + currentValue) + 32 : 32; /* Return the ascii array as a string */ return this.slice(fieldIndex, fieldIndex + fieldLength).asciiToString(); }; /* Return an array containing the value (as a string) of all the Data (D) subfield of the Data (D) field of an extended command packet */ Array.prototype.commandPacketAllDataFields = function () { let dataFieldSize = []; let index = 20 while(index < 32) dataFieldSize.push(this.slice(index, (index += 2)).asciiToHex()); return dataFieldSize.map((element) => this.slice(index, (index += element)).asciiToString()); } /* Return the value (as a number) of the Total number (CN) subfield of the Data (D) field of a data packet */ Array.prototype.dataPacketDataFieldGetTotalNumber = function () { return this.slice(0, 4).asciiToHex(); } /* Return the value (as a number) of the Current number (CN) subfield of the Data (D) field of a data packet */ Array.prototype.dataPacketDataFieldGetCurrentNumber = function () { return this.slice(4, 8).asciiToHex(); } /* Return the Data (D) subfield of the Data (D) field of a data packet */ Array.prototype.dataPacketDataFieldGetData = function () { return this.slice(8, this.length); } /* ---------------------Packet making--------------------- */ p7.make = { /* Create a basic (Non-Extended) packet * Return the packet */ basicPacket: (type, subtype) => ([type, //Type (T) ...subtype.hexToAscii(2), //Subtype (ST) 0x30].addP7Checksum()), //Extended (EX) /* Create an Extended packet * Return the packet */ extendedPacket: (type, subtype, dataField) => ([type, ...subtype.hexToAscii(2), 0x31, ...(dataField = dataField.encodeDataField()).length.hexToAscii(4), ...dataField].addP7Checksum()), /* Create a buffer containing the Data (D) field of a command packet * Return the buffer */ commandPacketDataField: (datatype, fileSize, commandArguments) => ([0x30, 0x30, //Overwrite (OW) ...datatype.hexToAscii(2), //Data type (DT) ?? ...fileSize.hexToAscii(8), //File size (FS) ...commandArguments.flatMap((element) => element.length.hexToAscii(2)), //Size of Data {1..6} (SD{1..6}) ...commandArguments.join('').toAscii()]), //Data {1..6} (D{1..6}) /* Create a buffer containing the Data (D) field of a data packet * Return the buffer */ dataPacketDataField: (totalNumber, currentNumber, data) => ([...totalNumber.hexToAscii(4), //Total number (TN) subfield ...currentNumber.hexToAscii(4), //Current number (CN) subfield ...data]), //Data (DD) subfield }; /* ---------------------Packet receiving--------------------- */ p7.receive = { packet: function () { console.log("%cReceiving...", "color: orange"); var packetInfo = {}; return p7.device.transferIn(2, 64) .then(function receive(transferInResult) { if (!transferInResult.data.byteLength) { console.log('buffer was empty, retrying'); return p7.device.transferIn(2, 64).then(receive); } packetInfo.receiveBuffer = Array.from(new Uint8Array(transferInResult.data.buffer)); packetInfo.length = 6; //Type (T), Subtype (S), Extended (EX) and Checksum (CS) fields //Set Type (T) field packetInfo.type = packetInfo.receiveBuffer[0]; //Set Subtype (ST) field packetInfo.subtype = packetInfo.receiveBuffer.slice(1, 2).asciiToHex(); //Set Extended (EX) field if ((packetInfo.extended = (packetInfo.receiveBuffer[3] === 0x31))) { console.log('packet is extended'); //Set Data size (DS) field packetInfo.dataSize = packetInfo.receiveBuffer.slice(4, 8).asciiToHex(); packetInfo.length += packetInfo.dataSize + 4; //Data size field if (64 < packetInfo.length) return p7.device.transferIn(2, packetInfo.length - 64) .then((transferInResult) => packetInfo.receiveBuffer.push(...(new Uint8Array(transferInResult.data.buffer)))); } }).then(() => { if (packetInfo.extended) packetInfo.data = packetInfo.receiveBuffer.slice(8, packetInfo.receiveBuffer.length - 2).decodeDataField(); //Compute checksum let checksumError = ((packetInfo.checksum = packetInfo.receiveBuffer.slice(packetInfo.length - 2, packetInfo.length).asciiToHex()) !== packetInfo.receiveBuffer.slice(1, packetInfo.length - 2).p7Checksum()); return packetInfo; }).catch((err) => { err.message = "Couldn't receive the packet: " + err.message; throw err; }); }, data: function (responsePacketInfo) { var data = []; return p7.send.packet(p7.make.basicPacket(p7PacketType.ack, ackSubtype.default)) .then(function receiveData(responsePacketInfo) { if (responsePacketInfo.type !== p7PacketType.data) return [data.flat(), responsePacketInfo]; data.push(responsePacketInfo.data.dataPacketDataFieldGetData()); return p7.send.packet(p7.make.basicPacket(p7PacketType.ack, ackSubtype.default)).then(receiveData); }); } }; /* ---------------------Packet sending--------------------- */ p7.send = { packet: function (buffer) { console.log("%cSending packet...", "color: green"); return p7.device.transferOut(1, Uint8Array.from(buffer)) .catch((err) => { err.message = "Couldn't send the packet: " + err.message; throw err; }) .then((transferOutResult) => p7.receive.packet()) .catch((err) => { err.message = "Couldn't receive the response packet: " + err.message; throw err; }); }, /* Send a single command * Return the informations about the response ack packet */ singleCommand: (subtype, datatype, fileSize, commandArguments) => p7.send.packet(p7.make.extendedPacket(p7PacketType.command, subtype, p7.make.commandPacketDataField(datatype, fileSize, commandArguments))) .then((responsePacketInfo) => { if (!(responsePacketInfo.type === p7PacketType.ack && responsePacketInfo.subtype === ackSubtype.default)) throw new Error("The calculator didn't send the expected ack packet!:" + "\t{Type: " + responsePacketInfo.type + "\tSubtype: " + responsePacketInfo.subtype + "}"); return responsePacketInfo; }), /* Send a single command request * Return an array containing the filtered (with `callbackFunction`) Data (D) field of each response command packet */ singleCommandRoleswap: function (subtype, datatype, fileSize, commandArguments, callbackFunction) { var filteredData = []; return p7.send.singleCommand(subtype, datatype, fileSize, commandArguments) .then(() => p7.send.packet(p7.make.basicPacket(p7PacketType.roleswap, roleswapSubtype.default))) .then(function receiveRequestedCommand(responsePacketInfo) { if (responsePacketInfo.type !== p7PacketType.command) return filteredData; filteredData.push(callbackFunction(responsePacketInfo.receiveBuffer)); return p7.send.packet(p7.make.basicPacket(p7PacketType.ack, ackSubtype.default)).then(receiveRequestedCommand); }); }, dataCommand: function (subtype, datatype, fileSize, commandArguments, data) { var currentPacketNumber = 0; var lastPacketSize = data.length & 0xFF; //Equivalent to data.length % 256, but way faster ! var totalPacketNumber = (data.length >> 8) + (lastPacketSize !== 0); //Equivalent to data.length / 256, but way way waaaaaay faster, and return an integer quotient, not a decimal one !! return p7.send.singleCommand(subtype, datatype, fileSize, commandArguments) .then(function sendDataPackets(result) { if ((currentPacketNumber += 1) === totalPacketNumber) return p7.send.packet(p7.make.extendedPacket(p7PacketType.data, subtype, p7.make.dataPacketDataField(totalPacketNumber, currentPacketNumber, data.slice(data.length - lastPacketSize, data.length)))) return p7.send.packet(p7.make.extendedPacket(p7PacketType.data, subtype, p7.make.dataPacketDataField(totalPacketNumber, currentPacketNumber, data.slice(((currentPacketNumber - 1) << 8), (currentPacketNumber << 8))))).then(sendDataPackets); }); }, dataCommandRoleswap: async function (subtype, datatype, fileSize, commandArguments, callbackFunction) { var filteredData = []; return p7.send.singleCommand(subtype, datatype, fileSize, commandArguments) .then(() => p7.send.packet(p7.make.basicPacket(p7PacketType.roleswap, roleswapSubtype.default))) .then(function receiveRequestedData(responsePacketInfo) { if (responsePacketInfo.type === p7PacketType.roleswap) return filteredData; let command = responsePacketInfo.receiveBuffer; return p7.receive.data() .then((dataResponse) => { filteredData.push(callbackFunction(command, dataResponse[0])); return receiveRequestedData(dataResponse[1]); }); }); } }; /* ---------------------Initialization and exiting--------------------- */ /* Connect to the calculator */ p7.connect = () => navigator.usb.requestDevice({filters:[ {'vendorId': 0x07cf , 'productId': 0x6101}, //fx-9750gII {'vendorId': 0x07cf , 'productId': 0x6102}] //fx-CP400 }).then((device) => { p7.device = device return p7.device.open(); }).then(() => { if (p7.device.configuration === null) return p7.device.selectConfiguration(1); //Only existing one so it should be selected by default, just checking }).then(() => { if (p7.device.configuration.interfaces[0].claimed === false) return p7.device.claimInterface(0); //Same as for the configuration }); /* Receive and send some usb control message, as defined in fxReverse documentation * * Adapted version (some parameters changed since fxReverse's writing) of usb_control_msg() function's protoype * from linux-manual : https://manpages.debian.org/jessie-backports/linux-manual-4.8/usb_control_msg.9.en.html * * int usb_control_msg(struct usb_device * dev, * uint8_t requesttype, * uint8_t request, * uint16_t value, * uint16_t index, * void * data, * uint16_t size, * int timeout); * * fxReverse2:13: * * int init_connection() { * char *buffer = calloc(0x29, sizeof(char)); * * //Receive 0x12 bytes of data * usb_control_message(usb_handle, * 0x80, = 0b10000000 * - D7 = 1 -> Data transfer direction: Device-to-host * - D6...D5 = 0 -> Type: Standard * - D4...D0 = 0 -> Recipient: Device * 0x6, * 0x0100, * 0x0, * buffer, * 0x12, * 200); * * //Same arguments (except timeout), but value is 0x200 and it receives 0x29 bytes of data * usb_control_message(usb_handle, 0x80, 0x6, 0x200, 0, buffer, 0x29, 250); * * * usb_control_message(usb_handle, * 0x41, = 0b01000001 * - D7 = 0 -> Data transfer direction: Host-to-device * - D6...D5 = 2 -> Type: Vendor * - D4...D0 = 1 -> Interface: Interface * 0x1, * 0x0, * 0, * buffer, * 0x0, * 250); * * free(buffer); * return 0; * } **************************************************************** let transferInResult = undefined; let controlData = undefined; try { transferInResult = await p7.device.controlTransferIn({ requestType: 'standard', recipient: 'device', request: 0x06, // GET_DESCRIPTOR value: 0x0100, // Descriptor Type and Descriptor Index index: 0x0000 }, 0x12); // Length } catch (err) { console.log(err); } controlData = new Uint8Array(transferInResult.data.buffer); console.log('vendor id : 0x' + controlData[9].toString(16).padStart(2, 0) + controlData[8].toString(16).padStart(2,0)); console.log('product id : 0x' + controlData[11].toString(16).padStart(2, 0) + controlData[10].toString(16).padStart(2,0)); try { transferInResult = await p7.device.controlTransferIn({ requestType: 'standard', recipient: 'device', request: 0x06, // GET_DESCRIPTOR value: 0x0200, // Descriptor Type and Descriptor Index index: 0x0000 }, 0x29); // Length } catch (err) { console.log(err); } controlData = new Uint8Array(transferInResult.data.buffer); console.log(controlData); try { await p7.device.controlTransferOut({ requestType: 'vendor', recipient: 'interface', request: 0x01, value: 0x0000, index: 0x0000 }, controlData); } catch (err) { console.log(err); } */ /* Initialize the connection with the calculator */ p7.init = () => p7.connect() .catch(() => { throw new Error("Couldn't connect to the calculator!"); }).then(() => p7.send.packet(p7.make.basicPacket(p7PacketType.check, checkSubtype.initialization)) .catch(() => { throw new Error("Couldn't send the initialisation packet!"); }).then(() => p7.send.packet(p7.make.basicPacket(p7PacketType.command, sysCommandSubtype.getDeviceInfo)) .catch(() => { throw new Error("Couldn't ask for the device infos!"); }).then((responsePacketInfo) => responsePacketInfo.receiveBuffer.extendedAckPacket()))); /* End the connexion with the calculator */ p7.exit = function () { } })();