this repo has no description

Add SonyToUnfoldedHex applet

harperandrews.org a0fe44fb 5563909b

verified
+160 -12
+74
src/applets/SonyToUnfoldedHex.ts
··· 1 + import { question, type PromptValidator } from "@topcli/prompts"; 2 + import type { Applet } from "../Applet.ts"; 3 + import { 4 + createIntPromptValidator, 5 + createUintPromptValidator, 6 + nonNegativeIntPromptValidator, 7 + } from "../util/prompts.ts"; 8 + import { 9 + combineBits, 10 + getMaxUnsignedInt, 11 + reverseBits, 12 + } from "../util/bit-manipulation.ts"; 13 + import { 14 + SONY_PROTOCOL_12_BITS, 15 + SONY_PROTOCOL_15_BITS, 16 + SONY_PROTOCOL_COMMAND_LENGTH_BITS, 17 + SONY_PROTOCOL_MAX_DEVICE_TYPE_LENGTH_BITS, 18 + } from "../util/sony.ts"; 19 + import { UNFOLDED_CIRCLE_PROTOCOL_SONY } from "../util/unfolded-circle.ts"; 20 + 21 + export const SonyToUnfoldedHexApplet: Applet = { 22 + name: "Convert Sony SIRC message to Unfolded Circle 'hex' IR code", 23 + runFunc: async (): Promise<undefined> => { 24 + const deviceType = Number.parseInt( 25 + await question("Enter the device type number", { 26 + validators: [ 27 + createUintPromptValidator( 28 + SONY_PROTOCOL_MAX_DEVICE_TYPE_LENGTH_BITS, 29 + `Does not fit in ${SONY_PROTOCOL_MAX_DEVICE_TYPE_LENGTH_BITS} bits (the 20-bit protocol is not supported at this time)`, 30 + ), 31 + ], 32 + }), 33 + ); 34 + const command = Number.parseInt( 35 + await question("Enter the command number", { 36 + validators: [ 37 + createUintPromptValidator( 38 + SONY_PROTOCOL_COMMAND_LENGTH_BITS, 39 + ), 40 + ], 41 + }), 42 + ); 43 + const repeatCount = Number.parseInt( 44 + await question( 45 + "How many times should the message repeat (by default)?", 46 + { 47 + defaultValue: "0", 48 + validators: [nonNegativeIntPromptValidator], 49 + }, 50 + ), 51 + ); 52 + 53 + const { totalLengthBits, deviceTypeLengthBits, commandLengthBits } = 54 + deviceType > SONY_PROTOCOL_12_BITS.deviceTypeLengthBits 55 + ? SONY_PROTOCOL_15_BITS 56 + : SONY_PROTOCOL_12_BITS; 57 + 58 + // We have to reverse the bits because SIRC transmits its fields LSB-first 59 + const data = combineBits([ 60 + { 61 + lengthBits: deviceTypeLengthBits, 62 + value: reverseBits(deviceType, deviceTypeLengthBits), 63 + }, 64 + { 65 + lengthBits: commandLengthBits, 66 + value: reverseBits(command, commandLengthBits), 67 + }, 68 + ]); 69 + 70 + console.log( 71 + `${UNFOLDED_CIRCLE_PROTOCOL_SONY};0x${data.toString(16).toUpperCase()};${totalLengthBits};${repeatCount}`, 72 + ); 73 + }, 74 + };
+4 -4
src/applets/UnfoldedHexToSony.ts
··· 1 1 import { question, type PromptValidator } from "@topcli/prompts"; 2 2 import type { Applet } from "../Applet.js"; 3 3 import { DATA_LENGTH_BITS_TO_SONY_PROTOCOL_VARIATION } from "../util/sony.ts"; 4 - import { extractNum } from "../util/bit-manipulation.ts"; 4 + import { extractBits } from "../util/bit-manipulation.ts"; 5 5 import { createStringPromptValidator } from "../util/prompts.ts"; 6 6 import { 7 7 parseUnfoldedHex, ··· 61 61 62 62 // We have to reverse the bits because SIRC transmits values LSB-first 63 63 // The device type is also referred to as the "address" 64 - const deviceType = extractNum({ 64 + const deviceType = extractBits({ 65 65 input: parsedUnfoldedHex.data, 66 66 leastSignificantBit: 0, 67 67 extractedNumLengthBits: deviceTypeLengthBits, 68 68 reverseResultBits: true, 69 69 }); 70 - const command = extractNum({ 70 + const command = extractBits({ 71 71 input: parsedUnfoldedHex.data, 72 72 leastSignificantBit: deviceTypeLengthBits, 73 73 extractedNumLengthBits: commandLengthBits, 74 74 reverseResultBits: true, 75 75 }); 76 - const extension = extractNum({ 76 + const extension = extractBits({ 77 77 input: parsedUnfoldedHex.data, 78 78 leastSignificantBit: deviceTypeLengthBits + commandLengthBits, 79 79 extractedNumLengthBits: extensionLengthBits,
+2 -1
src/main.ts
··· 2 2 import type { AppletRunFunc } from "./Applet.ts"; 3 3 import { ExitApplet } from "./applets/Exit.ts"; 4 4 import { UnfoldedHexToSonyApplet } from "./applets/UnfoldedHexToSony.ts"; 5 + import { SonyToUnfoldedHexApplet } from "./applets/SonyToUnfoldedHex.ts"; 5 6 6 - const applets = [UnfoldedHexToSonyApplet, ExitApplet]; 7 + const applets = [SonyToUnfoldedHexApplet, UnfoldedHexToSonyApplet, ExitApplet]; 7 8 const appletNames = applets.map((applet) => applet.name); 8 9 const appletsMap = applets.reduce( 9 10 (map, applet) => map.set(applet.name, applet.runFunc),
+16 -1
src/util/bit-manipulation.ts
··· 1 - export function extractNum(params: { 1 + export function extractBits(params: { 2 2 input: number; 3 3 extractedNumLengthBits: number; 4 4 leastSignificantBit: number; ··· 12 12 13 13 if (params.reverseResultBits) { 14 14 result = reverseBits(result, params.extractedNumLengthBits); 15 + } 16 + return result; 17 + } 18 + 19 + /** 20 + * Provide bit groups in order from least significant to most significant 21 + */ 22 + export function combineBits( 23 + bitGroups: { lengthBits: number; value: number }[], 24 + ): number { 25 + let result = 0; 26 + let nextBit = 0; 27 + for (const bitGroup of bitGroups) { 28 + result |= bitGroup.value << nextBit; 29 + nextBit += bitGroup.lengthBits; 15 30 } 16 31 return result; 17 32 }
+51
src/util/prompts.ts
··· 1 1 import type { PromptValidator } from "@topcli/prompts"; 2 + import { getMaxUnsignedInt } from "./bit-manipulation.ts"; 2 3 3 4 // TODO(Harper): Delete once https://github.com/TopCli/prompts/pull/151 is released 4 5 export type ValidationResponse = ··· 21 22 }, 22 23 }; 23 24 } 25 + 26 + export function createIntPromptValidator( 27 + validate?: (input: number) => ValidationResponse, 28 + ) { 29 + if (validate == undefined) { 30 + validate = () => ({ isValid: true }); 31 + } 32 + 33 + return createStringPromptValidator((input) => { 34 + // Number.parseInt() truncates floats, but we want to reject them 35 + const inputNumber = Number.parseFloat(input); 36 + if (Number.isNaN(inputNumber)) { 37 + return { isValid: false, error: "Input was not a number" }; 38 + } 39 + if (!Number.isInteger(inputNumber)) { 40 + return { isValid: false, error: "Input was not an integer" }; 41 + } 42 + return validate(inputNumber); 43 + }); 44 + } 45 + 46 + export function createUintPromptValidator( 47 + uintLengthBits: number, 48 + errorMsg?: string, 49 + ) { 50 + return createIntPromptValidator((input) => { 51 + if (input < getMaxUnsignedInt(uintLengthBits)) { 52 + return { isValid: true } as const; 53 + } else { 54 + return { 55 + isValid: false, 56 + error: 57 + errorMsg ?? `Input does not fit in ${uintLengthBits} bits`, 58 + } as const; 59 + } 60 + }); 61 + } 62 + 63 + export const nonNegativeIntPromptValidator = createIntPromptValidator( 64 + (input) => { 65 + if (input >= 0) { 66 + return { isValid: true } as const; 67 + } else { 68 + return { 69 + isValid: false, 70 + error: `Input was negative`, 71 + } as const; 72 + } 73 + }, 74 + );
+13 -6
src/util/sony.ts
··· 4 4 */ 5 5 6 6 export interface SonyProtocolVariation { 7 + totalLengthBits: 12 | 15 | 20; 7 8 commandLengthBits: 7; 8 9 deviceTypeLengthBits: 5 | 8; 9 10 extensionLengthBits: 0 | 8; 10 11 } 11 12 12 - const SONY_PROTOCOL_12_BITS: SonyProtocolVariation = { 13 - commandLengthBits: 7, 13 + export const SONY_PROTOCOL_COMMAND_LENGTH_BITS = 7; 14 + export const SONY_PROTOCOL_MAX_DEVICE_TYPE_LENGTH_BITS = 8; 15 + 16 + export const SONY_PROTOCOL_12_BITS: SonyProtocolVariation = { 17 + totalLengthBits: 12, 18 + commandLengthBits: SONY_PROTOCOL_COMMAND_LENGTH_BITS, 14 19 deviceTypeLengthBits: 5, 15 20 extensionLengthBits: 0, 16 21 }; 17 - const SONY_PROTOCOL_15_BITS: SonyProtocolVariation = { 18 - commandLengthBits: 7, 22 + export const SONY_PROTOCOL_15_BITS: SonyProtocolVariation = { 23 + totalLengthBits: 15, 24 + commandLengthBits: SONY_PROTOCOL_COMMAND_LENGTH_BITS, 19 25 deviceTypeLengthBits: 8, 20 26 extensionLengthBits: 0, 21 27 }; 22 - const SONY_PROTOCOL_20_BITS: SonyProtocolVariation = { 23 - commandLengthBits: 7, 28 + export const SONY_PROTOCOL_20_BITS: SonyProtocolVariation = { 29 + totalLengthBits: 20, 30 + commandLengthBits: SONY_PROTOCOL_COMMAND_LENGTH_BITS, 24 31 deviceTypeLengthBits: 5, 25 32 extensionLengthBits: 8, 26 33 };