Import elksmart-comm for transceiving IR codes
The receive functionality in the EXK4S is quite unstable, however useful enough for something that is officially unsupported. The gadgets are picky about cables and/or ports, but it has ridiculous reach when it works. This utility has previously been in the desktop-tools repository.
This commit is contained in:
parent
15ea5b6a8e
commit
26b38ecb88
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "liberty"]
|
||||
path = liberty
|
||||
url = https://git.janouch.name/p/liberty
|
71
CMakeLists.txt
Normal file
71
CMakeLists.txt
Normal file
@ -0,0 +1,71 @@
|
||||
cmake_minimum_required (VERSION 3.10)
|
||||
project (usb-drivers VERSION 0.1.0 DESCRIPTION "USB drivers" LANGUAGES C)
|
||||
|
||||
# Moar warnings
|
||||
set (CMAKE_C_STANDARD 99)
|
||||
set (CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set (CMAKE_C_EXTENSIONS OFF)
|
||||
|
||||
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
|
||||
# -Wunused-function is pretty annoying here, as everything is static
|
||||
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wno-unused-function")
|
||||
endif ()
|
||||
|
||||
# Dependencies
|
||||
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
|
||||
include (AddThreads)
|
||||
|
||||
find_package (PkgConfig REQUIRED)
|
||||
pkg_check_modules (libusb libusb-1.0)
|
||||
|
||||
option (WITH_LIBUSB "Compile with libusb utilities" ${libusb_FOUND})
|
||||
|
||||
# Generate a configuration file
|
||||
configure_file (${PROJECT_SOURCE_DIR}/config.h.in
|
||||
${PROJECT_BINARY_DIR}/config.h)
|
||||
include_directories (${PROJECT_BINARY_DIR})
|
||||
|
||||
# Build
|
||||
if ("${CMAKE_SYSTEM_NAME}" MATCHES BSD)
|
||||
# Need this for SIGWINCH in FreeBSD and OpenBSD respectively;
|
||||
# our POSIX version macros make it undefined
|
||||
add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
|
||||
elseif (APPLE)
|
||||
add_definitions (-D_DARWIN_C_SOURCE)
|
||||
endif ()
|
||||
|
||||
if (WITH_LIBUSB)
|
||||
list (APPEND targets elksmart-comm)
|
||||
add_executable (elksmart-comm elksmart-comm.c)
|
||||
target_include_directories (elksmart-comm PUBLIC ${libusb_INCLUDE_DIRS})
|
||||
target_link_directories (elksmart-comm PUBLIC ${libusb_LIBRARY_DIRS})
|
||||
target_link_libraries (elksmart-comm ${libusb_LIBRARIES})
|
||||
endif ()
|
||||
|
||||
# The files to be installed
|
||||
include (GNUInstallDirs)
|
||||
|
||||
# These should be accessible by users, but need to touch system devices.
|
||||
# Use the setuid bit, for simplicity.
|
||||
install (TARGETS ${targets} DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
PERMISSIONS
|
||||
OWNER_WRITE OWNER_READ OWNER_EXECUTE
|
||||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE
|
||||
SETUID)
|
||||
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
|
||||
|
||||
# CPack
|
||||
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
|
||||
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
|
||||
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
|
||||
set (CPACK_GENERATOR "TGZ;ZIP")
|
||||
set (CPACK_PACKAGE_FILE_NAME
|
||||
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
|
||||
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
|
||||
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
|
||||
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
|
||||
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
|
||||
|
||||
set (CPACK_SET_DESTDIR TRUE)
|
||||
include (CPack)
|
12
LICENSE
Normal file
12
LICENSE
Normal file
@ -0,0 +1,12 @@
|
||||
Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
53
README.adoc
Normal file
53
README.adoc
Normal file
@ -0,0 +1,53 @@
|
||||
USB drivers
|
||||
===========
|
||||
:compact-option:
|
||||
|
||||
_usb-drivers_ is a collection of utilities to control various hardware over USB:
|
||||
|
||||
- _eizoctl_ controls elementary settings of EIZO computer monitors.
|
||||
+
|
||||
It is intended as a replacement for official EIZO software,
|
||||
and may not run at the same time, as it would contend for device access.
|
||||
|
||||
- _eizoctltray_ is a derived Windows utility that can stay in the systray.
|
||||
|
||||
- _elksmart-comm_ interfaces with ELK Smart infrared dongles,
|
||||
partially reimplementing the Ocrustar mobile app.
|
||||
While it does not build for Windows, this is incidental.
|
||||
|
||||
Packages
|
||||
--------
|
||||
Regular releases are sporadic. git master should be stable enough.
|
||||
|
||||
Building
|
||||
--------
|
||||
Build dependencies: CMake, pkg-config, liberty (included) +
|
||||
Runtime dependencies: libusb-1.0 (elksmart-comm), hidapi (eizoctl)
|
||||
|
||||
$ git clone --recursive https://git.janouch.name/p/usb-drivers.git
|
||||
$ mkdir desktop-tools/build
|
||||
$ cd desktop-tools/build
|
||||
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug
|
||||
$ make
|
||||
|
||||
To install the applications, you can do either the usual:
|
||||
|
||||
# make install
|
||||
|
||||
Or you can try telling CMake to make a package for you. For Debian it is:
|
||||
|
||||
$ cpack -G DEB
|
||||
# dpkg -i usb-drivers-*.deb
|
||||
|
||||
Contributing and Support
|
||||
------------------------
|
||||
Use https://git.janouch.name/p/desktop-tools to report bugs, request features,
|
||||
or submit pull requests. `git send-email` is tolerated. If you want to discuss
|
||||
the project, feel free to join me at ircs://irc.janouch.name, channel #dev.
|
||||
|
||||
Bitcoin donations are accepted at: 12r5uEWEgcHC46xd64tt3hHt9EUvYYDHe9
|
||||
|
||||
License
|
||||
-------
|
||||
This software is released under the terms of the 0BSD license, the text of which
|
||||
is included within the package along with the list of authors.
|
8
config.h.in
Normal file
8
config.h.in
Normal file
@ -0,0 +1,8 @@
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
#define PROGRAM_NAME "${PROJECT_NAME}"
|
||||
#define PROGRAM_VERSION "${PROJECT_VERSION}"
|
||||
|
||||
#endif // ! CONFIG_H
|
||||
|
703
elksmart-comm.c
Normal file
703
elksmart-comm.c
Normal file
@ -0,0 +1,703 @@
|
||||
/*
|
||||
* elksmart-comm.c: ELK Smart infrared dongle tool (for EKX4S and EKX5S-T)
|
||||
*
|
||||
* Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and/or distribute this software for any
|
||||
* purpose with or without fee is hereby granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#undef PROGRAM_NAME
|
||||
#define PROGRAM_NAME "elksmart-comm"
|
||||
#include "liberty/liberty.c"
|
||||
|
||||
#include <libusb.h>
|
||||
|
||||
// --- Utilities ---------------------------------------------------------------
|
||||
|
||||
/// Search for a device with given vendor and product ID.
|
||||
/// This is quite similar to libusb_open_device_with_vid_pid().
|
||||
static libusb_device_handle *
|
||||
find_device (int vendor, int product, int *error)
|
||||
{
|
||||
libusb_device **list = NULL;
|
||||
libusb_device_handle *handle = NULL;
|
||||
int result = 0;
|
||||
|
||||
ssize_t len = libusb_get_device_list (NULL, &list);
|
||||
if (len < 0)
|
||||
{
|
||||
result = len;
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (ssize_t i = 0; i < len; i++)
|
||||
{
|
||||
libusb_device *device = list[i];
|
||||
struct libusb_device_descriptor desc = {};
|
||||
if ((result = libusb_get_device_descriptor (device, &desc)))
|
||||
print_debug ("%s", libusb_strerror (result));
|
||||
else if (desc.idVendor != vendor || desc.idProduct != product)
|
||||
continue;
|
||||
else if (!(result = libusb_open (device, &handle)))
|
||||
break;
|
||||
}
|
||||
|
||||
libusb_free_device_list (list, true);
|
||||
out:
|
||||
if (error != NULL && result != 0)
|
||||
*error = result;
|
||||
return handle;
|
||||
}
|
||||
|
||||
static void
|
||||
wait_ms (long ms)
|
||||
{
|
||||
struct timespec ts = { ms / 1000, (ms % 1000) * 1000 * 1000 };
|
||||
nanosleep (&ts, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
dump_hex (const unsigned char *buf, size_t len)
|
||||
{
|
||||
for (size_t i = 0; i < len; i++)
|
||||
printf ("%02x", buf[i]);
|
||||
printf ("\n");
|
||||
}
|
||||
|
||||
static bool
|
||||
read_hex (const char *string, struct str *out)
|
||||
{
|
||||
static const char *alphabet = "0123456789abcdef";
|
||||
str_reset (out);
|
||||
while (true)
|
||||
{
|
||||
while (*string && strchr (" \t\n\r\v\f", *string))
|
||||
string++;
|
||||
if (!*string)
|
||||
return true;
|
||||
|
||||
const char *hi, *lo;
|
||||
if (!(hi = strchr (alphabet, tolower_ascii (*string++))) || !*string
|
||||
|| !(lo = strchr (alphabet, tolower_ascii (*string++))))
|
||||
return false;
|
||||
|
||||
str_pack_u8 (out, (hi - alphabet) << 4 | (lo - alphabet));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Coding ------------------------------------------------------------------
|
||||
|
||||
// Values are in microseconds.
|
||||
struct pulse { unsigned on, off; };
|
||||
|
||||
static bool
|
||||
pulse_equal (struct pulse a, struct pulse b)
|
||||
{
|
||||
return a.on == b.on && a.off == b.off;
|
||||
}
|
||||
|
||||
static size_t
|
||||
decode_learned_direct (const uint8_t *b, size_t b_len, struct pulse *pulses)
|
||||
{
|
||||
size_t pulses_len = 0;
|
||||
for (size_t i = 0; i < b_len; )
|
||||
{
|
||||
struct pulse *pulse = &pulses[pulses_len++];
|
||||
while (b[i] == 0xff)
|
||||
{
|
||||
pulse->on += 4080;
|
||||
if (++i == b_len)
|
||||
return 0;
|
||||
}
|
||||
pulse->on += b[i++] * 16;
|
||||
|
||||
// Who cares, presumably it stays off.
|
||||
if (i == b_len)
|
||||
break;
|
||||
|
||||
while (b[i] == 0xff)
|
||||
{
|
||||
pulse->off += 4080;
|
||||
if (++i == b_len)
|
||||
return 0;
|
||||
}
|
||||
pulse->off += b[i++] * 16;
|
||||
}
|
||||
return pulses_len;
|
||||
}
|
||||
|
||||
static struct pulse *
|
||||
decode_learned (const struct str *code, size_t *len, struct error **e)
|
||||
{
|
||||
// This conveniently has an upper bound.
|
||||
struct pulse *pulses = xcalloc (code->len, sizeof *pulses);
|
||||
if (!(*len = decode_learned_direct
|
||||
((const uint8_t *) code->str, code->len, pulses)))
|
||||
{
|
||||
error_set (e, "code ends unexpectedly");
|
||||
free (pulses);
|
||||
return NULL;
|
||||
}
|
||||
return pulses;
|
||||
}
|
||||
|
||||
static struct pulse *
|
||||
encode_nec_byte (struct pulse *p, uint8_t byte)
|
||||
{
|
||||
for (int i = 7; i >= 0; i--)
|
||||
*p++ = (struct pulse)
|
||||
{ .on = 550, .off = ((byte >> i) & 1) ? 1650 : 550 };
|
||||
return p;
|
||||
}
|
||||
|
||||
static struct pulse *
|
||||
encode_nec (const struct str *code, size_t *len, struct error **e)
|
||||
{
|
||||
if (code->len % 2)
|
||||
{
|
||||
error_set (e, "NEC transmission format requires pairs");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// The timings seem to be rather tolerant.
|
||||
*len = code->len / 2 * (1 /* leader */ + 32 + 1 /* stop */);
|
||||
struct pulse *pulses = xcalloc (*len, sizeof *pulses), *p = pulses;
|
||||
for (size_t i = 0; i < code->len; i += 2)
|
||||
{
|
||||
*p++ = (struct pulse) { .on = 8500, .off = 4250 };
|
||||
p = encode_nec_byte (p, code->str[i + 0]);
|
||||
p = encode_nec_byte (p, ~code->str[i + 0]);
|
||||
p = encode_nec_byte (p, code->str[i + 1]);
|
||||
p = encode_nec_byte (p, ~code->str[i + 1]);
|
||||
*p++ = (struct pulse) { .on = 550, .off = 25000 };
|
||||
}
|
||||
return pulses;
|
||||
}
|
||||
|
||||
static void
|
||||
compress_value (unsigned value, struct str *encoded)
|
||||
{
|
||||
if (value <= 2032)
|
||||
{
|
||||
// We fix a minor problem in the original Ocrustar algorithm.
|
||||
uint8_t v = value / 16. + .5;
|
||||
str_pack_u8 (encoded, MAX (2, v));
|
||||
}
|
||||
else
|
||||
do
|
||||
{
|
||||
uint8_t v = value & 0x7f;
|
||||
if ((value >>= 7))
|
||||
v |= 0x80;
|
||||
str_pack_u8 (encoded, v);
|
||||
} while (value);
|
||||
}
|
||||
|
||||
static void
|
||||
compress_pulses (const struct pulse *pulses, size_t len, struct str *encoded)
|
||||
{
|
||||
unsigned counts[len];
|
||||
memset (counts, 0, sizeof counts);
|
||||
for (size_t i = 0; i < len; i++)
|
||||
for (size_t k = 0; k < len; k++)
|
||||
if (pulse_equal (pulses[i], pulses[k]))
|
||||
counts[i]++;
|
||||
|
||||
struct pulse p1 = {}, p2 = {};
|
||||
size_t top1 = 0, top2 = 0;
|
||||
for (size_t i = 0; i < len; i++)
|
||||
if (counts[i] > counts[top1])
|
||||
p1 = pulses[top1 = i];
|
||||
for (size_t i = 0; i < len; i++)
|
||||
if (counts[i] < counts[top1]
|
||||
&& counts[i] > counts[top2])
|
||||
p2 = pulses[top2 = i];
|
||||
else if (counts[top2] == counts[top1])
|
||||
p2 = pulses[top2 = i];
|
||||
|
||||
// Although I haven't really tried it, something tells me that
|
||||
// this will work even in the degenerated case of len <= 2.
|
||||
// XXX: The receiver might not like multibyte values here,
|
||||
// Ocrustar also oddly replaces 0xff with 0xfe for these fields.
|
||||
compress_value (p2.on, encoded);
|
||||
compress_value (p2.off, encoded);
|
||||
compress_value (p1.on, encoded);
|
||||
compress_value (p1.off, encoded);
|
||||
str_pack_u8 (encoded, -1);
|
||||
str_pack_u8 (encoded, -1);
|
||||
str_pack_u8 (encoded, -1);
|
||||
|
||||
for (size_t i = 0; i < len; i++)
|
||||
{
|
||||
if (pulse_equal (pulses[i], p1))
|
||||
str_pack_u8 (encoded, 0);
|
||||
else if (pulse_equal (pulses[i], p2))
|
||||
str_pack_u8 (encoded, 1);
|
||||
else
|
||||
{
|
||||
compress_value (pulses[i].on, encoded);
|
||||
compress_value (pulses[i].off, encoded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Device interaction ------------------------------------------------------
|
||||
|
||||
enum
|
||||
{
|
||||
USB_VENDOR_SMTCTL = 0x045c,
|
||||
|
||||
// 0x134 (EKX5S ~ 5s, 5th generation remote)
|
||||
// 0x195 (EKX4S ~ 4s, 4th generation remote)
|
||||
// 0x184 (EKX5S-T, international edition)
|
||||
USB_PRODUCT_SMTCTL_SMART_EKX4S = 0x0195,
|
||||
USB_PRODUCT_SMTCTL_SMART_EKX5S_T = 0x0184,
|
||||
|
||||
// There should only ever be one interface.
|
||||
USB_INTERFACE = 0,
|
||||
};
|
||||
|
||||
static uint8_t
|
||||
c_transmit[] = { -1, -1, -1, -1 },
|
||||
c_learn[] = { -2, -2, -2, -2 },
|
||||
c_stop[] = { -3, -3, -3, -3 },
|
||||
c_identify[] = { -4, -4, -4, -4 };
|
||||
|
||||
static struct
|
||||
{
|
||||
unsigned char endpoint_out; ///< Outgoing endpoint
|
||||
unsigned char endpoint_in; ///< Incoming endpoint
|
||||
}
|
||||
g;
|
||||
|
||||
static bool
|
||||
init_device_from_desc (struct libusb_config_descriptor *desc, struct error **e)
|
||||
{
|
||||
// We're not being particuarly strict in here.
|
||||
if (desc->bNumInterfaces != 1)
|
||||
return error_set (e, "unexpected USB interface count");
|
||||
if (desc->interface->num_altsetting != 1)
|
||||
return error_set (e, "unexpected alternate setting count");
|
||||
|
||||
const struct libusb_interface_descriptor *asd = desc->interface->altsetting;
|
||||
if (asd->bInterfaceClass != LIBUSB_CLASS_COMM)
|
||||
return error_set (e, "unexpected USB interface class");
|
||||
if (asd->bNumEndpoints != 2)
|
||||
return error_set (e, "unexpected endpoint count");
|
||||
|
||||
bool have_out = false, have_in = false;
|
||||
for (uint8_t i = 0; i < asd->bNumEndpoints; i++)
|
||||
{
|
||||
const struct libusb_endpoint_descriptor *epd = asd->endpoint + i;
|
||||
if ((epd->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK)
|
||||
!= LIBUSB_ENDPOINT_TRANSFER_TYPE_BULK)
|
||||
return error_set (e, "unexpected endpoint transfer type");
|
||||
|
||||
switch ((epd->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK))
|
||||
{
|
||||
break; case LIBUSB_ENDPOINT_OUT:
|
||||
have_out = true;
|
||||
g.endpoint_out = epd->bEndpointAddress;
|
||||
break; case LIBUSB_ENDPOINT_IN:
|
||||
have_in = true;
|
||||
g.endpoint_in = epd->bEndpointAddress;
|
||||
}
|
||||
}
|
||||
if (!have_out || !have_in)
|
||||
return error_set (e, "USB interface is not bidirectional");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
init_device (libusb_device_handle *device, struct error **e)
|
||||
{
|
||||
struct libusb_config_descriptor *desc = NULL;
|
||||
int result =
|
||||
libusb_get_active_config_descriptor (libusb_get_device (device), &desc);
|
||||
if (result)
|
||||
return error_set (e, "%s", libusb_strerror (result));
|
||||
|
||||
bool ok = true;
|
||||
if ((result = libusb_kernel_driver_active (device, USB_INTERFACE)) == 1)
|
||||
ok = error_set (e, "device is claimed by a kernel driver");
|
||||
else if (result)
|
||||
ok = error_set (e, "%s", libusb_strerror (result));
|
||||
else
|
||||
ok = init_device_from_desc (desc, e);
|
||||
|
||||
libusb_free_config_descriptor (desc);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static uint8_t
|
||||
mangle (uint8_t value)
|
||||
{
|
||||
uint8_t reversed = 0;
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
reversed = (reversed << 1) | (value & 1);
|
||||
value >>= 1;
|
||||
}
|
||||
return ~reversed;
|
||||
}
|
||||
|
||||
static uint8_t
|
||||
checksum (const uint8_t *b, size_t len)
|
||||
{
|
||||
uint32_t sum = 0;
|
||||
for (size_t i = 0; i < len; i++)
|
||||
sum += b[i];
|
||||
return mangle ((sum & 0xF0) | ((sum >> 8) & 0x0F));
|
||||
}
|
||||
|
||||
static bool
|
||||
send_transmit (libusb_device_handle *device, unsigned long frequency,
|
||||
const struct pulse *pulses, size_t pulses_len, struct error **e)
|
||||
{
|
||||
if (g_debug_mode)
|
||||
for (size_t i = 0; i < pulses_len; )
|
||||
{
|
||||
printf ("%u,%u", pulses[i].on, pulses[i].off);
|
||||
putchar (++i == pulses_len ? '\n' : ',');
|
||||
}
|
||||
|
||||
struct str compressed = str_make ();
|
||||
compress_pulses (pulses, pulses_len, &compressed);
|
||||
|
||||
struct str message = str_make ();
|
||||
str_append_data (&message, c_transmit, sizeof c_transmit);
|
||||
frequency += 0x7ffff;
|
||||
str_pack_u8 (&message, mangle (frequency >> 8));
|
||||
str_pack_u8 (&message, mangle (frequency >> 16));
|
||||
str_pack_u8 (&message, mangle (frequency));
|
||||
str_pack_u8 (&message, mangle (compressed.len >> 8));
|
||||
str_pack_u8 (&message, mangle (compressed.len));
|
||||
str_append_str (&message, &compressed);
|
||||
str_free (&compressed);
|
||||
|
||||
size_t i = 0;
|
||||
uint8_t buffer[64];
|
||||
bool ok = true;
|
||||
while (i != message.len)
|
||||
{
|
||||
size_t chunk = MIN (62, message.len - i);
|
||||
memcpy (buffer, message.str + i, chunk);
|
||||
i += chunk;
|
||||
if (chunk == 62)
|
||||
{
|
||||
buffer[chunk] = checksum (buffer, chunk);
|
||||
chunk++;
|
||||
}
|
||||
|
||||
int result = 0, len = 0;
|
||||
if ((result = libusb_bulk_transfer (device, g.endpoint_out,
|
||||
buffer, chunk, &len, 100)))
|
||||
{
|
||||
ok = error_set (e, "send: %s", libusb_strerror (result));
|
||||
break;
|
||||
}
|
||||
wait_ms (2);
|
||||
}
|
||||
str_free (&message);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static bool
|
||||
pulse_is_likely_leader (const struct pulse *p)
|
||||
{
|
||||
return p->on >= 2048 && p->off >= 2048;
|
||||
}
|
||||
|
||||
static void
|
||||
try_to_depulse (const struct str *code)
|
||||
{
|
||||
size_t len = 0;
|
||||
struct pulse *pulses = decode_learned (code, &len, NULL);
|
||||
if (!pulses)
|
||||
return;
|
||||
|
||||
struct pulse *p = pulses, *end = p + len;
|
||||
while (p != end && pulse_is_likely_leader (p))
|
||||
{
|
||||
p++;
|
||||
printf ("Attempted pulse decode:\n");
|
||||
|
||||
uint8_t bits = 0, nibble = 0;
|
||||
for (; p != end && !pulse_is_likely_leader (p); p++)
|
||||
{
|
||||
nibble = nibble << 1 | (p->off > 2 * p->on);
|
||||
if (++bits == 4)
|
||||
{
|
||||
putchar ("0123456789abcdef"[nibble]);
|
||||
bits = nibble = 0;
|
||||
}
|
||||
}
|
||||
putchar ('\n');
|
||||
}
|
||||
free (pulses);
|
||||
}
|
||||
|
||||
static bool
|
||||
recv_learn (libusb_device_handle *device, struct str *data, struct error **e)
|
||||
{
|
||||
uint8_t buffer[64] = {};
|
||||
int result = 0, len = 0;
|
||||
while ((result = libusb_bulk_transfer (device, g.endpoint_in,
|
||||
buffer, sizeof buffer, &len, 100)))
|
||||
{
|
||||
if (result != LIBUSB_ERROR_TIMEOUT)
|
||||
return error_set (e, "learn/recv: %s", libusb_strerror (result));
|
||||
print_debug ("learn/recv: %s", libusb_strerror (result));
|
||||
}
|
||||
if (len < 6 || memcmp (buffer, c_learn, sizeof c_learn))
|
||||
return error_set (e, "learn/recv: %s", "unexpected response");
|
||||
|
||||
// This field might only make sense for a later device,
|
||||
// because it doesn't always correspond with how much data we receive.
|
||||
// Nonetheless, it does match exactly often enough.
|
||||
size_t size = buffer[4] << 8 | buffer[5];
|
||||
print_debug ("learn: code size: %zu", size);
|
||||
|
||||
str_append_data (data, buffer + 6, len - 6);
|
||||
dump_hex ((const unsigned char *) data->str, data->len);
|
||||
while (data->len < size)
|
||||
{
|
||||
if (!(result = libusb_bulk_transfer (device, g.endpoint_in,
|
||||
buffer, sizeof buffer, &len, 100)))
|
||||
{
|
||||
dump_hex (buffer, len);
|
||||
|
||||
str_append_data (data, buffer, len);
|
||||
print_debug ("learn: received %d (have %zu of %zu)",
|
||||
len, data->len, size);
|
||||
continue;
|
||||
}
|
||||
if (result != LIBUSB_ERROR_TIMEOUT)
|
||||
return error_set (e, "learn/recv: %s", libusb_strerror (result));
|
||||
|
||||
// The device seems to queue up its output with pauses.
|
||||
print_debug ("learn/recv: %s", libusb_strerror (result));
|
||||
}
|
||||
|
||||
// As far as I know, this doesn't do anything,
|
||||
// and the device doesn't accept it while scanning infrared codes either.
|
||||
if ((result = libusb_bulk_transfer (device, g.endpoint_out,
|
||||
c_stop, sizeof c_stop, &len, 100)))
|
||||
return error_set (e, "learn/send: %s", libusb_strerror (result));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
send_learn (libusb_device_handle *device, struct error **e)
|
||||
{
|
||||
int result = 0, len = 0;
|
||||
if ((result = libusb_bulk_transfer (device, g.endpoint_out,
|
||||
c_learn, sizeof c_learn, &len, 100)))
|
||||
return error_set (e, "learn/send: %s", libusb_strerror (result));
|
||||
|
||||
printf ("Reading remote control codes.\n");
|
||||
printf ("Press a remote control button from less than a centimeter.\n");
|
||||
printf ("The dongle may be unusable until it returns some data.\n");
|
||||
// ... Resetting the device using libusb_reset_device() doesn't help then.
|
||||
printf ("If the code fails to replay, retry the capture.\n");
|
||||
|
||||
struct str data = str_make ();
|
||||
bool ok = recv_learn (device, &data, e);
|
||||
if (ok)
|
||||
{
|
||||
printf ("Full command:\n");
|
||||
dump_hex ((const unsigned char *) data.str, data.len);
|
||||
try_to_depulse (&data);
|
||||
}
|
||||
|
||||
str_free (&data);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static bool
|
||||
send_identify (libusb_device_handle *device, struct error **e)
|
||||
{
|
||||
uint8_t buffer[64] = {};
|
||||
int result = 0, len = 0;
|
||||
while (!(result = libusb_bulk_transfer (device, g.endpoint_in,
|
||||
buffer, sizeof buffer, &len, 10)))
|
||||
/* Flush buffers. */;
|
||||
|
||||
if ((result = libusb_bulk_transfer (device, g.endpoint_out,
|
||||
c_identify, sizeof c_identify, &len, 100)))
|
||||
return error_set (e, "identify/send: %s", libusb_strerror (result));
|
||||
if ((result = libusb_bulk_transfer (device, g.endpoint_in,
|
||||
buffer, sizeof buffer, &len, 100)))
|
||||
return error_set (e, "identify/recv: %s", libusb_strerror (result));
|
||||
|
||||
// XXX: Sometimes, the device doesn't send any identification values.
|
||||
if (len != 6 || memcmp (buffer, c_identify, sizeof c_identify)
|
||||
|| buffer[4] != 0x70 || buffer[5] != 0x01)
|
||||
return error_set (e, "device busy or not supported");
|
||||
|
||||
#if 0
|
||||
// The EKX4S does not respond to this request.
|
||||
static uint8_t c_serial[] = { -5, -5, -5, -5 };
|
||||
if ((result = libusb_bulk_transfer (device, g.endpoint_out,
|
||||
c_serial, sizeof c_serial, &len, 100)))
|
||||
return error_set (e, "serial/send: %s", libusb_strerror (result));
|
||||
if ((result = libusb_bulk_transfer (device, g.endpoint_in,
|
||||
buffer, sizeof buffer, &len, 100)))
|
||||
return error_set (e, "serial/recv: %s", libusb_strerror (result));
|
||||
if (len < (int) sizeof c_serial
|
||||
|| memcmp (buffer, c_serial, sizeof c_serial))
|
||||
return error_set (e, "serial retrieval failed");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
run (libusb_device_handle *device, unsigned long frequency, bool nec,
|
||||
char **codes, size_t codes_len, struct error **e)
|
||||
{
|
||||
if (!send_identify (device, e))
|
||||
return false;
|
||||
if (!codes_len)
|
||||
return send_learn (device, e);
|
||||
|
||||
struct str code = str_make ();
|
||||
bool ok = true;
|
||||
for (size_t i = 0; i < codes_len; i++)
|
||||
{
|
||||
if (!read_hex (codes[i], &code))
|
||||
{
|
||||
ok = error_set (e, "invalid hex string");
|
||||
break;
|
||||
}
|
||||
|
||||
size_t pulses_len = 0;
|
||||
struct pulse *pulses = nec
|
||||
? encode_nec (&code, &pulses_len, e)
|
||||
: decode_learned (&code, &pulses_len, e);
|
||||
|
||||
ok = pulses && send_transmit (device, frequency, pulses, pulses_len, e);
|
||||
free (pulses);
|
||||
if (!ok)
|
||||
break;
|
||||
|
||||
wait_ms (100);
|
||||
}
|
||||
str_free (&code);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// --- Main --------------------------------------------------------------------
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
unsigned long frequency = 38000;
|
||||
bool nec = false;
|
||||
static const struct opt opts[] =
|
||||
{
|
||||
{ 'd', "debug", NULL, 0, "run in debug mode" },
|
||||
{ 'f', "frequency", "HZ", 0, "frequency (38000 Hz by default)" },
|
||||
{ 'n', "nec", NULL, 0, "use the NEC transmission format" },
|
||||
{ 'h', "help", NULL, 0, "display this help and exit" },
|
||||
{ 'V', "version", NULL, 0, "output version information and exit" },
|
||||
{ 0, NULL, NULL, 0, NULL }
|
||||
};
|
||||
|
||||
struct opt_handler oh = opt_handler_make (argc, argv, opts, "[COMMAND...]",
|
||||
"Transmit or receive infrared commands.");
|
||||
|
||||
int c;
|
||||
while ((c = opt_handler_get (&oh)) != -1)
|
||||
switch (c)
|
||||
{
|
||||
case 'd':
|
||||
g_debug_mode = true;
|
||||
break;
|
||||
case 'f':
|
||||
if (!xstrtoul (&frequency, optarg, 10) || !frequency)
|
||||
exit_fatal ("invalid frequency");
|
||||
break;
|
||||
case 'n':
|
||||
nec = true;
|
||||
break;
|
||||
case 'h':
|
||||
opt_handler_usage (&oh, stdout);
|
||||
exit (EXIT_SUCCESS);
|
||||
case 'V':
|
||||
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
|
||||
exit (EXIT_SUCCESS);
|
||||
default:
|
||||
print_error ("wrong options");
|
||||
opt_handler_usage (&oh, stderr);
|
||||
exit (EXIT_FAILURE);
|
||||
}
|
||||
|
||||
argc -= optind;
|
||||
argv += optind;
|
||||
|
||||
opt_handler_free (&oh);
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
#if LIBUSB_API_VERSION >= 0x0100010A
|
||||
const struct libusb_init_option option =
|
||||
{
|
||||
.option = LIBUSB_OPTION_LOG_LEVEL,
|
||||
.value.ival = LIBUSB_LOG_LEVEL_DEBUG,
|
||||
};
|
||||
int result = libusb_init_context (NULL, &option, g_debug_mode);
|
||||
#else
|
||||
int result = libusb_init (NULL);
|
||||
#endif
|
||||
if (result)
|
||||
exit_fatal ("libusb: %s", libusb_strerror (result));
|
||||
|
||||
libusb_device_handle *device = NULL;
|
||||
if (!device && !result)
|
||||
device = find_device (USB_VENDOR_SMTCTL,
|
||||
USB_PRODUCT_SMTCTL_SMART_EKX4S, &result);
|
||||
if (!device && !result)
|
||||
device = find_device (USB_VENDOR_SMTCTL,
|
||||
USB_PRODUCT_SMTCTL_SMART_EKX5S_T, &result);
|
||||
|
||||
if (result)
|
||||
exit_fatal ("couldn't open device: %s", libusb_strerror (result));
|
||||
else if (!device)
|
||||
exit_fatal ("no suitable device found");
|
||||
|
||||
struct error *e = NULL;
|
||||
if (!init_device (device, &e))
|
||||
exit_fatal ("%s", e->message);
|
||||
if ((result = libusb_claim_interface (device, USB_INTERFACE)) == 1)
|
||||
exit_fatal ("couldn't claim interface: %s", libusb_strerror (result));
|
||||
|
||||
if (!run (device, frequency, nec, argv, argc, &e))
|
||||
{
|
||||
print_error ("%s", e->message);
|
||||
error_free (e);
|
||||
}
|
||||
|
||||
if ((result = libusb_release_interface (device, USB_INTERFACE)) == 1)
|
||||
exit_fatal ("couldn't release interface: %s", libusb_strerror (result));
|
||||
|
||||
libusb_close (device);
|
||||
libusb_exit (NULL);
|
||||
return 0;
|
||||
}
|
1
liberty
Submodule
1
liberty
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 492815c8fc38ad6e333b2f1c5094a329e3076155
|
Loading…
Reference in New Issue
Block a user