package com.silicondust.libhdhomerun;
import com.silicondust.libhdhomerun.HDHomerun_Sock;
import com.silicondust.libhdhomerun.HDHomerun_Sock.hdhomerun_local_ip_info_t;
public class HDHomerun_Discover {
public class hdhomerun_discover_device_t {
public int ip_addr; // uint32_t
public int device_type; // uint32_t
public int device_id; // uint32_t
public short tuner_count; // uint8_t
public hdhomerun_discover_device_t() {
reset();
}
boolean compare(hdhomerun_discover_device_t r) {
return (ip_addr == r.ip_addr & device_type == r.device_type && device_id == r.device_id && tuner_count == r.tuner_count);
}
void reset() {
ip_addr = 0;
device_type = 0;
device_id = 0;
tuner_count = 0;
}
};
private final static int HDHOMERUN_DISOCVER_MAX_SOCK_COUNT = 16;
private static class hdhomerun_discover_sock_t {
HDHomerun_Sock sock;
boolean detected; // bool_t
int local_ip; // uint32_t
int subnet_mask; // uint32_t
public void set(hdhomerun_discover_sock_t r) {
sock = r.sock;
detected = r.detected;
local_ip = r.local_ip;
subnet_mask = r.subnet_mask;
}
};
private hdhomerun_discover_sock_t[] socks = new hdhomerun_discover_sock_t[HDHOMERUN_DISOCVER_MAX_SOCK_COUNT];
private int sock_count = 0; // unsigned int
private HDHomerun_Pkt tx_pkt = new HDHomerun_Pkt();
private HDHomerun_Pkt rx_pkt = new HDHomerun_Pkt();
private boolean mIsValid = false;
private boolean sock_add(int local_ip, int subnet_mask)
{
for (int i = 1; i < sock_count; i++) {
hdhomerun_discover_sock_t dss = socks[i];
if ((dss.local_ip == local_ip) && (dss.subnet_mask == subnet_mask)) {
dss.detected = true;
return true;
}
}
if (sock_count >= HDHOMERUN_DISOCVER_MAX_SOCK_COUNT) {
return false;
}
/* Create socket. */
HDHomerun_Sock sock = null;
try {
sock = new HDHomerun_Sock(local_ip, (short) 0, 0, 0, false);
} catch (Exception e) {
if(local_ip != 0)
return false;
}
finally {
if (local_ip > 0 && (sock == null || !sock.isValid())) {
return false;
}
}
/* Write sock entry. */
socks[sock_count] = new hdhomerun_discover_sock_t();
hdhomerun_discover_sock_t dss = socks[sock_count++];
dss.sock = sock;
dss.detected = true;
dss.local_ip = local_ip;
dss.subnet_mask = subnet_mask;
return true;
}
/*
* Optional: persistent discover instance available for discover polling use.
*/
public HDHomerun_Discover()
{
/* Create a routable socket (always first entry). */
if (sock_add(0, 0)) {
/* Success. */
mIsValid = true;
}
}
public boolean isValid() {
return mIsValid;
}
public void destroy()
{
for (int i = 0; i < sock_count; i++) {
hdhomerun_discover_sock_t dss = socks[i];
dss.sock.destroy();
socks[i] = null;
}
}
private void sock_detect()
{
for (int i = 1; i < sock_count; i++)
socks[i].detected = false;
hdhomerun_local_ip_info_t[] ip_info_list = new hdhomerun_local_ip_info_t[HDHOMERUN_DISOCVER_MAX_SOCK_COUNT];
int count = HDHomerun_Sock.hdhomerun_local_ip_info(ip_info_list, HDHOMERUN_DISOCVER_MAX_SOCK_COUNT);
if (count < 0) {
count = 0;
}
for (int index = 0; index < count; index++) {
hdhomerun_local_ip_info_t ip_info = ip_info_list[index];
sock_add(ip_info.ip_addr, ip_info.subnet_mask);
}
hdhomerun_discover_sock_t src = socks[1];
int currSrc = 1;
hdhomerun_discover_sock_t dst = socks[1];
int currDst = 1;
count = 1;
for (int i = 1; i < sock_count; i++) {
if (!src.detected) {
src.sock.destroy();
src = socks[currSrc++];
continue;
}
if (dst != src) {
dst.set(src);
}
src = socks[currSrc++];
dst = socks[currDst++];
count++;
}
sock_count = count;
}
public int find_devices(int target_ip, int device_type, int device_id, hdhomerun_discover_device_t result_list[], int max_count)
{
sock_detect();
int count = 0;
int attempt;
for (attempt = 0; attempt < 2; attempt++) {
if (!send(target_ip, device_type, device_id)) {
return -1;
}
long timeout = HDHomerun_OS.getcurrenttime() + 200;
while (true) {
result_list[count] = new hdhomerun_discover_device_t();
hdhomerun_discover_device_t result = result_list[count];
result.reset();
if (!recv(result)) {
if (HDHomerun_OS.getcurrenttime() >= timeout) {
break;
}
HDHomerun_OS.msleep_approx(10);
continue;
}
/* Filter. */
if (device_type != HDHomerun_Pkt.HDHOMERUN_DEVICE_TYPE_WILDCARD) {
if (device_type != result.device_type) {
continue;
}
}
if (device_id != HDHomerun_Pkt.HDHOMERUN_DEVICE_ID_WILDCARD) {
if (device_id != result.device_id) {
continue;
}
}
/* Ensure not already in list. */
if (null != find_in_list(result_list, count, result)) {
continue;
}
/* Add to list. */
count++;
if (count >= max_count) {
return count;
}
}
}
return count;
}
private boolean send_target_ip(int target_ip, int device_type, int device_id)
{
boolean result = false;
/*
* Send targeted packet from any local ip that is in the same subnet.
* This will work with multiple separate 169.254.x.x interfaces.
*/
for (int i = 1; i < sock_count; i++) {
hdhomerun_discover_sock_t dss = socks[i];
if ((target_ip & dss.subnet_mask) != (dss.local_ip & dss.subnet_mask)) {
continue;
}
result |= send_internal(dss, target_ip, device_type, device_id);
}
/*
* If target IP does not match a local subnet then fall back to letting the OS choose the gateway interface.
*/
if (!result) {
hdhomerun_discover_sock_t dss = socks[0];
result = send_internal(dss, target_ip, device_type, device_id);
}
return result;
}
private boolean send_internal(hdhomerun_discover_sock_t dss, int target_ip, int device_type, int device_id)
{
tx_pkt.reset();
tx_pkt.write_u8(HDHomerun_Pkt.HDHOMERUN_TAG_DEVICE_TYPE);
tx_pkt.write_var_length((short) 4);
tx_pkt.write_u32(device_type);
tx_pkt.write_u8(HDHomerun_Pkt.HDHOMERUN_TAG_DEVICE_ID);
tx_pkt.write_var_length((short) 4);
tx_pkt.write_u32(device_id);
tx_pkt.seal_frame(HDHomerun_Pkt.HDHOMERUN_TYPE_DISCOVER_REQ);
return dss.sock.sendto(target_ip, HDHomerun_Pkt.HDHOMERUN_DISCOVER_UDP_PORT, tx_pkt.buffer, tx_pkt.startIndex, tx_pkt.endIndex - tx_pkt.startIndex, 0);
}
private boolean send_wildcard_ip(int device_type, int device_id)
{
boolean result = false;
/*
* Send subnet broadcast using each local ip socket.
* This will work with multiple separate 169.254.x.x interfaces.
*/
for (int i = 1; i < sock_count; i++) {
hdhomerun_discover_sock_t dss = socks[i];
int target_ip = dss.local_ip | ~dss.subnet_mask;
result |= send_internal(dss, target_ip, device_type, device_id);
}
/*
* If no local ip sockets then fall back to sending a global broadcast letting the OS choose the interface.
*/
if (!result) {
hdhomerun_discover_sock_t dss = socks[0];
result = send_internal(dss, 0xFFFFFFFF, device_type, device_id);
}
return result;
}
public boolean send(int target_ip, int device_type, int device_id)
{
if (target_ip == 0) {
return send_wildcard_ip(device_type, device_id);
} else {
return send_target_ip(target_ip, device_type, device_id);
}
}
private hdhomerun_discover_device_t find_in_list(hdhomerun_discover_device_t result_list[], int count, hdhomerun_discover_device_t lookup)
{
for (int index = 0; index < count; index++) {
hdhomerun_discover_device_t entry = result_list[index];
if (entry.compare(lookup)) {
return entry;
}
}
return null;
}
private boolean recv_internal(hdhomerun_discover_sock_t dss, hdhomerun_discover_device_t result)
{
rx_pkt.reset();
int[] remote_addr = new int[1];
int[] remote_port = new int[1];
int[] length = new int[1];
length[0]= rx_pkt.limitIndex - rx_pkt.endIndex;
if (!dss.sock.recvfrom(remote_addr, remote_port, rx_pkt.buffer, rx_pkt.endIndex, length, 0)) {
return false;
}
rx_pkt.endIndex += length[0];
short[] type = new short[1];
if (rx_pkt.open_frame(type) <= 0) {
return false;
}
if (type[0] != HDHomerun_Pkt.HDHOMERUN_TYPE_DISCOVER_RPY) {
return false;
}
result.ip_addr = remote_addr[0];
result.device_type = 0;
result.device_id = 0;
result.tuner_count = 0;
while (true) {
byte[] tag = new byte[1];
int[] len = new int[1];
int next = rx_pkt.read_tlv(tag, len);
if (next == 0) {
break;
}
switch (tag[0]) {
case HDHomerun_Pkt.HDHOMERUN_TAG_DEVICE_TYPE:
if (len[0] != 4) {
break;
}
result.device_type = rx_pkt.read_u32();
break;
case HDHomerun_Pkt.HDHOMERUN_TAG_DEVICE_ID:
if (len[0] != 4) {
break;
}
result.device_id = rx_pkt.read_u32();
break;
case HDHomerun_Pkt.HDHOMERUN_TAG_TUNER_COUNT:
if (len[0] != 1) {
break;
}
result.tuner_count = rx_pkt.read_u8();
break;
default:
break;
}
rx_pkt.posIndex = (short) next;
}
/* Fixup for old firmware. */
if (result.tuner_count == 0) {
switch (result.device_id >> 20) {
case 0x102:
result.tuner_count = 1;
break;
case 0x100:
case 0x101:
case 0x121:
result.tuner_count = 2;
break;
default:
break;
}
}
return true;
}
private boolean recv(hdhomerun_discover_device_t result)
{
for (int i = 0; i < sock_count; i++) {
hdhomerun_discover_sock_t dss = socks[i];
if (recv_internal(dss, result)) {
return true;
}
}
return false;
}
/*
* Verify that the device ID given is valid.
*
* The device ID contains a self-check sequence that detects common user input errors including
* single-digit errors and two digit transposition errors.
*
* Returns TRUE if valid.
* Returns FALSE if not valid.
*/
private static final int[] lookup_table = new int[] {0xA, 0x5, 0xF, 0x6, 0x7, 0xC, 0x1, 0xB, 0x9, 0x2, 0x8, 0xD, 0x4, 0x3, 0xE, 0x0};
public static boolean hdhomerun_discover_validate_device_id(int device_id)
{
int checksum = 0;
checksum ^= lookup_table[(device_id >> 28) & 0x0F];
checksum ^= (device_id >> 24) & 0x0F;
checksum ^= lookup_table[(device_id >> 20) & 0x0F];
checksum ^= (device_id >> 16) & 0x0F;
checksum ^= lookup_table[(device_id >> 12) & 0x0F];
checksum ^= (device_id >> 8) & 0x0F;
checksum ^= lookup_table[(device_id >> 4) & 0x0F];
checksum ^= (device_id >> 0) & 0x0F;
return (checksum == 0);
}
/*
* Detect if an IP address is multicast.
*
* Returns TRUE if multicast.
* Returns FALSE if zero, unicast, expermental, or broadcast.
*/
public static boolean hdhomerun_discover_is_ip_multicast(int ip_addr)
{
return (ip_addr >= 0xE0000000) && (ip_addr < 0xF0000000);
}
/*
* Find devices.
*
* The device information is stored in caller-supplied array of hdhomerun_discover_device_t vars.
* Multiple attempts are made to find devices.
* Execution time is typically 400ms if max_count is not reached.
*
* Set target_ip to zero to auto-detect the IP address.
* Set device_type to HDHOMERUN_DEVICE_TYPE_TUNER to detect HDHomeRun tuner devices.
* Set device_id to HDHOMERUN_DEVICE_ID_WILDCARD to detect all device ids.
*
* Returns the number of devices found.
* Retruns -1 on error.
*/
public static int hdhomerun_discover_find_devices_custom(int target_ip, int device_type, int device_id, hdhomerun_discover_device_t result_list[], int max_count)
{
if (hdhomerun_discover_is_ip_multicast(target_ip)) {
return 0;
}
HDHomerun_Discover ds = new HDHomerun_Discover();
if (null == ds || !ds.isValid()) {
return -1;
}
int ret = ds.find_devices(target_ip, device_type, device_id, result_list, max_count);
ds = null;
return ret;
}
}