{"action":"created","comment":{"url":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/comments/31920794","id":31920794,"diff_hunk":"@@ -5,146 +5,973 @@ angular.module('rdmApp', [])\n   $interpolateProvider.endSymbol('}]}');\n  }])\n \n- .controller('UIDController', ['$scope', function(convertor) {\n+ .constant('RDM', {\n+  'COMMAND_CLASS': {\n+   'DISCOVERY_COMMAND': 0x10,\n+   'DISCOVERY_COMMAND_RESPONSE': 0x11,\n+   'GET_COMMAND': 0x20,\n+   'GET_COMMAND_RESPONSE': 0x21,\n+   'SET_COMMAND': 0x30,\n+   'SET_COMMAND_RESPONSE': 0x31\n+  },\n+  'EUID_SIZE': 16,\n+  'NACK_REASON': {\n+   'NR_UNKNOWN_PID': 0x0,\n+   'NR_FORMAT_ERROR': 0x1,\n+   'NR_HARDWARE_FAULT': 0x2,\n+   'NR_PROXY_REJECT': 0x3,\n+   'NR_WRITE_PROTECT': 0x4,\n+   'NR_UNSUPPORTED_COMMAND_CLASS': 0x5,\n+   'NR_DATA_OUT_OF_RANGE': 0x6,\n+   'NR_BUFFER_FULL': 0x7,\n+   'NR_PACKET_SIZE_UNSUPPORTED': 0x8,\n+   'NR_SUB_DEVICE_OUT_OF_RANGE': 0x9,\n+   'NR_PROXY_BUFFER_FULL': 0x10,\n+   'NR_ACTION_NOT_SUPPORTED': 0x11\n+  },\n+  'PIDS': {\n+   'DISC_UNIQUE_BRANCH': 0x0001,\n+   'DISC_MUTE': 0x0002,\n+   'DISC_UN_MUTE': 0x0003,\n+   'PROXIED_DEVICES': 0x0010,\n+   'PROXIED_DEVICE_COUNT': 0x0011,\n+   'COMMS_STATUS': 0x0015,\n+   'QUEUED_MESSAGE': 0x0020,\n+   'STATUS_MESSAGES': 0x0030,\n+   'STATUS_ID_DESCRIPTION': 0x0031,\n+   'CLEAR_STATUS_ID': 0x0032,\n+   'SUB_DEVICE_STATUS_REPORT_THRESHOLD': 0x0033,\n+   'SUPPORTED_PARAMETERS': 0x0050,\n+   'PARAMETER_DESCRIPTION': 0x0051,\n+   'DEVICE_INFO': 0x0060,\n+   'PRODUCT_DETAIL_ID_LIST': 0x0070,\n+   'DEVICE_MODEL_DESCRIPTION': 0x0080,\n+   'MANUFACTURER_LABEL': 0x0081,\n+   'DEVICE_LABEL': 0x0082,\n+   'FACTORY_DEFAULTS': 0x0090,\n+   'LANGUAGE_CAPABILITIES': 0x00a0,\n+   'LANGUAGE': 0x00b0,\n+   'SOFTWARE_VERSION_LABEL': 0x00c0,\n+   'BOOT_SOFTWARE_VERSION_ID': 0x00c1,\n+   'BOOT_SOFTWARE_VERSION_LABEL': 0x00c2,\n+   'DMX_PERSONALITY': 0x00E0,\n+   'DMX_PERSONALITY_DESCRIPTION': 0x00e1,\n+   'DMX_START_ADDRESS': 0x00f0,\n+   'SLOT_INFO': 0x0120,\n+   'SLOT_DESCRIPTION': 0x0121,\n+   'DEFAULT_SLOT_VALUE': 0x0122,\n+   'SENSOR_DEFINITION': 0x0200,\n+   'SENSOR_VALUE': 0x0201,\n+   'RECORD_SENSORS': 0x0202,\n+   'DEVICE_HOURS': 0x0400,\n+   'LAMP_HOURS': 0x0401,\n+   'LAMP_STRIKES': 0x0402,\n+   'LAMP_STATE': 0x0403,\n+   'LAMP_ON_MODE': 0x0404,\n+   'DEVICE_POWER_CYCLES': 0x0405,\n+   'DISPLAY_INVERT': 0x0500,\n+   'DISPLAY_LEVEL': 0x0501,\n+   'PAN_INVERT': 0x0600,\n+   'TILT_INVERT': 0x0601,\n+   'PAN_TILT_SWAP': 0x0602,\n+   'REAL_TIME_CLOCK': 0x0603,\n+   'IDENTIFY_DEVICE': 0x1000,\n+   'RESET_DEVICE': 0x1001,\n+   'POWER_STATE': 0x1010,\n+   'PERFORM_SELFTEST': 0x1020,\n+   'SELF_TEST_DESCRIPTION': 0x1021,\n+   'CAPTURE_PRESET': 0x1030,\n+   'PRESET_PLAYBACK': 0x1031,\n+   'DMX_BLOCK_ADDRESS': 0x0140,\n+   'DMX_FAIL_MODE': 0x0141,\n+   'DMX_STARTUP_MODE': 0x0142,\n+   'DIMMER_INFO': 0x0340,\n+   'MINIMUM_LEVEL': 0x0341,\n+   'MAXIMUM_LEVEL': 0x0342,\n+   'CURVE': 0x0343,\n+   'CURVE_DESCRIPTION': 0x0344,\n+   'OUTPUT_RESPONSE_TIME': 0x0345,\n+   'OUTPUT_RESPONSE_TIME_DESCRIPTION': 0x0346,\n+   'MODULATION_FREQUENCY': 0x0347,\n+   'MODULATION_FREQUENCY_DESCRIPTION': 0x0348,\n+   'BURN_IN': 0x0440,\n+   'LOCK_PIN': 0x0640,\n+   'LOCK_STATE': 0x0641,\n+   'LOCK_STATE_DESCRIPTION': 0x0642,\n+   'IDENTIFY_MODE': 0x1040,\n+   'PRESET_INFO': 0x1041,\n+   'PRESET_STATUS': 0x1042,\n+   'PRESET_MERGEMODE': 0x1043,\n+   'POWER_ON_SELF_TEST': 0x1044,\n+   'LIST_INTERFACES': 0x0700,\n+   'INTERFACE_LABEL': 0x0701,\n+   'INTERFACE_HARDWARE_ADDRESS_TYPE1': 0x0702,\n+   'IPV4_DHCP_MODE': 0x0703,\n+   'IPV4_ZEROCONF_MODE': 0x0704,\n+   'IPV4_CURRENT_ADDRESS': 0x0705,\n+   'IPV4_STATIC_ADDRESS': 0x0706,\n+   'INTERFACE_RENEW_DHCP': 0x0707,\n+   'INTERFACE_RELEASE_DHCP': 0x0708,\n+   'INTERFACE_APPLY_CONFIGURATION': 0x0709,\n+   'IPV4_DEFAULT_ROUTE': 0x070a,\n+   'DNS_NAME_SERVER': 0x070b,\n+   'DNS_HOSTNAME': 0x070c,\n+   'DNS_DOMAIN_NAME': 0x070d\n+  },\n+  'RESPONSE_TYPE': {\n+   'ACK': 0,\n+   'ACK_TIMER': 1,\n+   'NACK': 2,\n+   'ACK_OVERFLOW': 3\n+  },\n+  'START_CODE': 0xcc,\n+  'SUB_DEVICE': {\n+   'ROOT_DEVICE': 0,\n+   'ALL_SUB_DEVICES': 0xffff\n+  },\n+  'UID_SIZE': 6\n+ })\n+\n+ .constant('OUTPUT_FORMAT', {\n+  'C_ARRAY': {'label': 'C Array', 'value': 0},\n+  'C_STRING': {'label': 'C String', 'value': 1},\n+  'RAW_HEX': {'label': 'Raw Hex', 'value': 2}\n+ })\n+\n+ .service('checksumService', function() {\n+   'use strict';\n+   var checksum = function(input) {\n+    var sum = input.reduce(\n+     function(previousValue, currentValue, index, array) {\n+      return previousValue + currentValue;\n+     }\n+    );\n+    return sum;\n+   };\n+\n+   this.checksumAsArray = function(input) {\n+    var sum = checksum(input);\n+    return [sum >> 8, sum & 0xff];\n+   };\n+\n+   this.checksumAsValue = checksum;\n+  })\n+\n+ .service('rdmHelperService', ['RDM', function(RDM) {\n+   'use strict';\n+   this.isRequest = function(command_class) {\n+    return command_class === RDM.COMMAND_CLASS.DISCOVERY_COMMAND ||\n+           command_class === RDM.COMMAND_CLASS.GET_COMMAND ||\n+           command_class === RDM.COMMAND_CLASS.SET_COMMAND;\n+   };\n+\n+   this.isResponse = function(command_class) {\n+    return command_class === RDM.COMMAND_CLASS.DISCOVERY_COMMAND_RESPONSE ||\n+           command_class === RDM.COMMAND_CLASS.GET_COMMAND_RESPONSE ||\n+           command_class === RDM.COMMAND_CLASS.SET_COMMAND_RESPONSE;\n+   };\n+\n+   this.isGetSet = function(command_class) {\n+    return command_class === RDM.COMMAND_CLASS.GET_COMMAND ||\n+           command_class === RDM.COMMAND_CLASS.GET_COMMAND_RESPONSE ||\n+           command_class === RDM.COMMAND_CLASS.SET_COMMAND ||\n+           command_class === RDM.COMMAND_CLASS.SET_COMMAND_RESPONSE;\n+   };\n+  }])\n+\n+ .service('formatService', ['RDM', function(RDM) {\n+   'use strict';\n+   var pad = function(length) {\n+    return new Array(length + 1).join(' ');\n+   };\n+\n+   var toHex = function(num, places, prefix, suffix) {\n+    var str = num.toString(16);\n+    var zero = places - str.length + 1;\n+    return ((prefix ? prefix : '') +\n+            new Array(+(zero > 0 && zero)).join('0') +\n+            str + (suffix ? suffix : ''));\n+   };\n+   this.toHex = toHex;\n+\n+   this.reverseLookup = function(dictionary, needle) {\n+    var item = '';\n+    angular.forEach(dictionary, function(value, key) {\n+     if (value === needle) {\n+      item = key;\n+     }\n+    });\n+    return item;\n+   };\n+\n+   this.arrayToUID = function(data) {\n+    if (data.length !== RDM.UID_SIZE) {\n+     return '';\n+    }\n+    return toHex(data[0], 2) + toHex(data[1], 2) + ':' +\n+           toHex(data[2], 2) + toHex(data[3], 2) +\n+           toHex(data[4], 2) + toHex(data[5], 2);\n+   };\n+\n+   this.arrayToHex = function(input, prefix, suffix) {\n+    return input.map(function(i) {\n+     return toHex(i, 2, prefix, suffix);\n+    });\n+   };\n+\n+   this.dataAsArray = function(data, wrap) {\n+    var indent = 2;\n+    var prefix = 'const uint8_t packet[] = {';\n+    var lines = [prefix];\n+    var current_line = pad(2);\n+    for (var i = 0; i < data.length; ++i) {\n+     var str = toHex(data[i], 2, '0x');\n+     if (current_line.length + str.length + 2 >= wrap) {\n+      current_line += ',';\n+      lines.push(current_line);\n+      current_line = pad(indent);\n+     } else if (current_line.length !== indent) {\n+      current_line += ', ';\n+     }\n+     current_line += str;\n+    }\n+    if (current_line) {\n+     lines.push(current_line);\n+    }\n+    lines.push('};');\n+    return lines.join('\\n');\n+   };\n+\n+   this.dataAsString = function(data, wrap) {\n+    var lines = [];\n+    var prefix = 'const char packet[] = \"';\n+    var current_line = prefix;\n+    for (var i = 0; i < data.length; ++i) {\n+     var str = toHex(data[i], 2, '\\\\x');\n+     if (current_line.length + str.length + 2 >= wrap) {\n+      current_line += '\"';\n+      lines.push(current_line);\n+      current_line = pad(prefix.length - 1) + '\"';\n+     }\n+     current_line += str;\n+    }\n+    if (current_line) {\n+     current_line += '\";';\n+     lines.push(current_line);\n+    }\n+    return lines.join('\\n');\n+   };\n+\n+   this.dataAsRawHex = function(data, wrap) {\n+     var lines = [];\n+     var current_line = '';\n+     for (var i = 0; i < data.length; ++i) {\n+      var str = toHex(data[i], 2);\n+      if (current_line.length + str.length + 1 >= wrap) {\n+       lines.push(current_line);\n+       current_line = '';\n+      }\n+      current_line += str;\n+      if (i + 1 !== data.length) {\n+       current_line += ' ';\n+      }\n+     }\n+     if (current_line) {\n+      lines.push(current_line);\n+     }\n+     return lines.join('\\n');\n+    };\n+  }])\n+\n+ .service('parserService', ['$log', 'RDM', function($log, RDM) {\n+   'use strict';\n+   var guessDataFormat = function(tokens) {\n+    /*\n+     * Try to determine how to interpret digits like '10'. We do this by\n+     * looking at the other data supplied.\n+     * This works well for RDM packets, because the start code is either 0xCC\n+     * or 204 so we only have to check the first token.\n+     */\n+    for (var j = 0; j < tokens.length; j++) {\n+     // If any of the tokens contain hex characters, default to hex.\n+     var token = tokens[j];\n+     if (token.match(/^[a-fA-F]{1,2}$/)) {\n+      return true;\n+     }\n+\n+     // If any tokens are 3 digits, default to decimal.\n+     if (token.match(/^[\\d]{3}$/)) {\n+      return true;\n+     }\n+\n+     // If any tokens are in the form ##h or 0x##, assume tokens not in this\n+     // format are decimal.\n+     if (token.match(/^[\\da-fA-F]{1,2}h$/) ||\n+         token.match(/^0x[\\da-fA-F]{1,2}$/)) {\n+      return false;\n+     }\n+    }\n+    // default to decimal\n+    return false;\n+   };\n+\n+   var parseLines = function(lines, as_hex) {\n+    // If as_hex isn't defined we try to be clever about what we accept.\n+    // ##h and 0x## are obviously hex values, ## is ambiguous so we use\n+    // heuristics to figure it out.\n+    var error = '';\n+    var binary_data = [];\n+\n+    var tokens = [];\n+    for (var i = 0; i < lines.length; ++i) {\n+     var line = lines[i].replace(/^(.*?)\\s*\\/\\/.*$/, '$1');\n+     line = line.replace(/[,:\\-]/g, ' ');\n+     line = line.replace(/\\s{2,}/g, ' ').trim();\n+     if (line) {\n+      tokens = tokens.concat(line.split(' '));\n+     }\n+    }\n+\n+    if (as_hex === undefined) {\n+     as_hex = guessDataFormat(tokens);\n+    }\n+\n+    for (var j = 0; j < tokens.length; j++) {\n+     var token = tokens[j];\n+     var hex_suffix_match = token.match(/^([\\da-fA-F]{1,2})h$/);\n+     if (hex_suffix_match) {\n+      binary_data.push(parseInt(hex_suffix_match[0], 16));\n+      continue;\n+     }\n+\n+     var hex_prefix_match = token.match(/^0x([\\da-fA-F]{1,2})$/);\n+     if (hex_prefix_match) {\n+      binary_data.push(parseInt(hex_prefix_match[0], 16));\n+      continue;\n+     }\n+\n+     if (as_hex) {\n+      var hex_match = token.match(/^([\\da-fA-F]{1,2})$/);\n+      if (hex_match) {\n+       binary_data.push(parseInt(hex_match[0], 16));\n+       continue;\n+      } else {\n+       error = 'Invalid byte: ' + token;\n+       return [error, binary_data];\n+      }\n+     } else {\n+      var decimal_match = token.match(/^(\\d{1,3})$/);\n+      if (decimal_match) {\n+       if (decimal_match[0] <= 255) {\n+        binary_data.push(parseInt(decimal_match[0], 10));\n+        continue;\n+       } else {\n+        error = 'Invalid byte: ' + token;\n+        return [error, binary_data];\n+       }\n+      }\n+     }\n+\n+     error = 'Invalid binary data: ' + token;\n+    }\n+    return [error, binary_data];\n+   };\n+\n+   this.textToBytes = function(text, as_hex) {\n+     var lines = text.split('\\n');\n+     return parseLines(lines, as_hex);\n+    };\n+\n+   this.uidToBytes = function(text) {\n+    var clean_uid = text.replace(/[\\.:\\-\\s]/g, '');\n+    if (!clean_uid.match(/^[0-9a-fA-F]{12}$/)) {\n+     return ['Contains non hex characters', []];\n+    }\n+\n+    if (clean_uid.length !== RDM.UID_SIZE * 2) {\n+     return ['UID should be ' + RDM.UID_SIZE + ' bytes', []];\n+    }\n+\n+    var uid_bytes = [];\n+    for (var i = 0; i < clean_uid.length / 2; i++) {\n+     var octet = clean_uid.slice(i * 2, (i + 1) * 2);\n+     var value = parseInt(octet, 16);\n+     if (isNaN(value)) {\n+      return ['Invalid UID: bad value' + octet, []];\n+     }\n+     uid_bytes.push(value);\n+    }\n+\n+    return ['', uid_bytes];\n+   };\n+  }])\n+\n+ .controller('UIDController',\n+             ['$scope', '$log', 'checksumService', 'formatService',\n+              'parserService',\n+              function($scope, $log, checksumService, formatService,\n+                       parserService) {\n   'use strict';\n-  convertor.invalid_input_message = 'Invalid UID, please enter a UID in the ' +\n+  var OUTPUT_FORMATS = {\n+   'HEX_SUFFIX': {'label': '##h', 'value': 0},\n+   'HEX_PREFIX': {'label': '0x##', 'value': 1},\n+   'HEX_PAIRS': {'label': 'Hex Pairs (##)', 'value': 2},\n+   'DECIMAL_PAIRS': {'label': 'Decimal Pairs (##)', 'value': 3}\n+  };\n+\n+  $scope.invalid_input_message = 'Invalid UID, please enter a UID in the ' +\n    'form MMMM:NNNNNNNN';\n-  convertor.euid = '';\n-  convertor.error = '';\n-  convertor.uid = '';\n+  $scope.uid = '';\n+  $scope.euid = '';\n+  $scope.error = '';\n+  $scope.OUTPUT_FORMATS = OUTPUT_FORMATS;\n+  $scope.format = OUTPUT_FORMATS.HEX_PAIRS.value;\n \n-  convertor.convertToEUID = function() {\n-   convertor.euid = '';\n-   convertor.error = '';\n+  $scope.convertToEUID = function() {\n+   $scope.euid = '';\n+   $scope.error = '';\n \n-   var clean_uid = convertor.uid.replace(/[\\.:\\-\\s]/g, '');\n-   if (!clean_uid.match(/^[0-9a-fA-F]{12}$/)) {\n-    convertor.error = convertor.invalid_input_message;\n+   var return_data = parserService.uidToBytes($scope.uid);\n+   if (return_data[0]) {\n+    $scope.error = $scope.invalid_input_message;\n     return;\n    }\n+   var uid_bytes = return_data[1];\n \n-   var manufacturer_str = clean_uid.slice(0, 4);\n-   var device_str = clean_uid.slice(4, 12);\n+   var euid_bytes = [];\n+   angular.forEach(uid_bytes, function(value, index) {\n+    this.push(value | 0xaa);\n+    this.push(value | 0x55);\n+   }, euid_bytes);\n+\n+   var checksum = checksumService.checksumAsArray(euid_bytes);\n+\n+   angular.forEach(checksum, function(value, index) {\n+    this.push(value | 0xaa);\n+    this.push(value | 0x55);\n+   }, euid_bytes);\n \n-   if (manufacturer_str.length !== 4 || device_str.length !== 8) {\n-    convertor.error = convertor.invalid_input_message;\n+   if ($scope.format === OUTPUT_FORMATS.HEX_SUFFIX.value) {\n+    $scope.euid = formatService.arrayToHex(euid_bytes, '', 'h').join(' ');\n+   } else if ($scope.format === OUTPUT_FORMATS.HEX_PREFIX.value) {\n+    $scope.euid = formatService.arrayToHex(euid_bytes, '0x').join(' ');\n+   } else if ($scope.format === OUTPUT_FORMATS.HEX_PAIRS.value) {\n+    $scope.euid = formatService.arrayToHex(euid_bytes).join(' ');\n+   } else {\n+    $scope.euid = euid_bytes.join(' ');\n+   }\n+  };\n+ }])\n+\n+ .controller('EUIDController',\n+             ['$scope', '$log', 'checksumService', 'formatService',\n+              'parserService', 'RDM',\n+              function($scope, $log, checksumService, formatService,\n+                       parserService, RDM) {\n+  'use strict';\n+  var INPUT_FORMATS = {\n+   'DECIMAL': {'label': 'Decimal', 'value': 0},\n+   'HEX': {'label': 'Hexadecimal', 'value': 1}\n+  };\n+\n+  $scope.INPUT_FORMATS = INPUT_FORMATS;\n+  $scope.format = INPUT_FORMATS.HEX.value;\n+\n+  $scope.euid = '';\n+  $scope.error = '';\n+  $scope.uid = '';\n+\n+  $scope.convertToUID = function() {\n+   $scope.error = '';\n+   $scope.uid = '';\n+\n+   var return_data = parserService.textToBytes(\n+       $scope.euid, $scope.format === INPUT_FORMATS.HEX.value);\n+\n+   if (return_data[0]) {\n+    $scope.error = 'Invalid EUID: ' + return_data[0];\n     return;\n    }\n \n-   var manufacturer = parseInt(manufacturer_str, 16);\n-   var device = parseInt(device_str, 16);\n-   if (isNaN(manufacturer) || isNaN(device)) {\n-    convertor.error = convertor.invalid_input_message;\n+   var data = return_data[1];\n+\n+   if (data.length !== RDM.EUID_SIZE) {\n+    $scope.error = 'Invalid EUID: insufficent data, should be 16 bytes';\n     return;\n    }\n \n-   var euid_bytes = [];\n+   var uid_array = [\n+    data[0] & data[1],\n+    data[2] & data[3],\n+    data[4] & data[5],\n+    data[6] & data[7],\n+    data[8] & data[9],\n+    data[10] & data[11]\n+   ];\n+\n+   var recovered_checksum = ((data[12] & data[13]) << 8) +\n+    (data[14] & data[15]);\n+\n+   var calculated_checksum = checksumService.checksumAsValue(data.slice(0, 12));\n+   if (recovered_checksum !== calculated_checksum) {\n+    $scope.error = 'Checksum mismatch, was ' + recovered_checksum +\n+    ', calculated to be ' + calculated_checksum;\n+    return;\n+   }\n+\n+   $scope.uid = formatService.arrayToUID(uid_array);\n+  };\n+ }])\n+\n+ .controller('RDMPacketBuilder',\n+             ['$scope', '$log', 'checksumService', 'parserService',\n+              'formatService', 'rdmHelperService', 'OUTPUT_FORMAT',\n+              'RDM',\n+              function($scope, $log, checksumService, parserService,\n+                       formatService, rdmHelperService, OUTPUT_FORMAT,\n+                       RDM) {\n+  'use strict';\n+  $scope.packet = {\n+    'start_code': RDM.START_CODE,\n+    'sub_start_code': 1,\n+    'dest_uid': '7a70:00000000',\n+    'src_uid': '7a70:12345678',\n+    'transaction_number': 0,\n+    'port_id': 0,\n+    'message_count': 0,\n+    'sub_device': 0,\n+    'command_class': RDM.COMMAND_CLASS.DISCOVERY_COMMAND,\n+    'param_id': 0,\n+    'response_type': 0,\n+    'nack_reason': 0,\n+    'ack_timer': 0,\n+    'param_data': '',\n+    'lower_uid': '',\n+    'upper_uid': ''\n+   };\n+\n+  $scope.properties = {\n+   'is_request': false,\n+   'show_param_data': false,\n+   'pids': []\n+  };\n+\n+  $scope.settings = {\n+    'output_format': OUTPUT_FORMAT.C_ARRAY.value,\n+    'wrap': 80\n+   };\n+\n+  $scope.RDM = RDM;\n+  $scope.command_classes = [];\n+  angular.forEach(RDM.COMMAND_CLASS, function(value, key) {\n+    this.push({\n+      'value': value,\n+      'label': key + ' (' + formatService.toHex(value, 2, '0x') + ')'\n+     });\n+   }, $scope.command_classes);\n+  $scope.command_classes.sort(function(a, b) {\n+   return (( a.value === b.value ) ? 0 : ( ( a.value > b.value ) ? 1 : -1 ));\n+  });\n+\n+  $scope.OUTPUT_FORMAT = {};\n+  angular.forEach(OUTPUT_FORMAT, function(value, key) {\n+    this[value.label] = value.value;\n+   }, $scope.OUTPUT_FORMAT);\n+\n+  $scope.output = '';\n+  $scope.error = '';\n \n-   var manufacturer0 = (manufacturer >> 8) & 0xff;\n-   var manufacturer1 = manufacturer & 0xff;\n-   var device0 = (device >> 24) & 0xff;\n-   var device1 = (device >> 16) & 0xff;\n-   var device2 = (device >> 8) & 0xff;\n-   var device3 = device & 0xff;\n-\n-   euid_bytes.push(manufacturer0 | 0xaa);\n-   euid_bytes.push(manufacturer0 | 0x55);\n-   euid_bytes.push(manufacturer1 | 0xaa);\n-   euid_bytes.push(manufacturer1 | 0x55);\n-\n-   euid_bytes.push(device0 | 0xaa);\n-   euid_bytes.push(device0 | 0x55);\n-   euid_bytes.push(device1 | 0xaa);\n-   euid_bytes.push(device1 | 0x55);\n-   euid_bytes.push(device2 | 0xaa);\n-   euid_bytes.push(device2 | 0x55);\n-   euid_bytes.push(device3 | 0xaa);\n-   euid_bytes.push(device3 | 0x55);\n-\n-   var checksum = euid_bytes.reduce(\n-    function(previousValue, currentValue, index, array) {\n-     return previousValue + currentValue;\n-    }\n-   );\n-   var checksum0 = (checksum >> 8) & 0xff;\n-   var checksum1 = checksum & 0xff;\n-\n-   euid_bytes.push(checksum0 | 0xaa);\n-   euid_bytes.push(checksum0 | 0x55);\n-   euid_bytes.push(checksum1 | 0xaa);\n-   euid_bytes.push(checksum1 | 0x55);\n-\n-   convertor.euid = euid_bytes.map(function(i) {\n-    return i.toString(16);\n-   }).join(' ');\n+  var parseUIDOrSetError = function(uid, uid_name) {\n+   var parse_return = parserService.uidToBytes(uid);\n+   if (parse_return[0]) {\n+    $scope.error = 'Invalid ' + uid_name + ' UID:' + parse_return[0];\n+    return '';\n+   }\n+   return parse_return[1];\n+  };\n+\n+  var showHideParamData = function() {\n+   $scope.properties.show_param_data = (\n+       rdmHelperService.isGetSet($scope.packet.command_class) &&\n+       ($scope.packet.response_type === RDM.RESPONSE_TYPE.ACK ||\n+        $scope.packet.response_type === RDM.RESPONSE_TYPE.ACK_OVERFLOW));\n   };\n+\n+  var commandClassChanged = function(new_value, old_value, scope) {\n+    $scope.packet.command_class = new_value;\n+    $scope.properties.is_request = rdmHelperService.isRequest(new_value);\n+    showHideParamData();\n+\n+    var pid_names = [];\n+    if (new_value === RDM.COMMAND_CLASS.DISCOVERY_COMMAND) {\n+     pid_names = ['DISC_UNIQUE_BRANCH', 'DISC_MUTE', 'DISC_UN_MUTE'];\n+    } else if (new_value === RDM.COMMAND_CLASS.DISCOVERY_COMMAND_RESPONSE) {\n+     pid_names = ['DISC_MUTE', 'DISC_UN_MUTE'];\n+    } else {\n+     var discovery_pids = ['DISC_UNIQUE_BRANCH', 'DISC_MUTE', 'DISC_UN_MUTE'];\n+     angular.forEach(RDM.PIDS, function(value, key) {\n+       if (discovery_pids.indexOf(key) === -1) {\n+        this.push(key);\n+       }\n+      }, pid_names);\n+    }\n+\n+    var found_selected = false;\n+    var pids = [];\n+    angular.forEach(pid_names, function(key, index) {\n+     found_selected |= $scope.packet.param_id === RDM.PIDS[key];\n+     var label = key + ' (' + formatService.toHex(RDM.PIDS[key], 4, '0x') + ')';\n+     this.push({'label': label, 'value': RDM.PIDS[key]});\n+    }, pids);\n+\n+    pids.sort(function(a, b) {\n+     return (( a.label === b.label ) ? 0 : ( ( a.label > b.label ) ? 1 : -1 ));\n+    });\n+\n+    if (!found_selected) {\n+     $scope.packet.param_id = pids[0].value;\n+    }\n+    $scope.properties.pids = pids;\n+   };\n+\n+  var responseTypeChanged = function(newValue, oldValue, scope) {\n+    $scope.packet.response_type = newValue;\n+    showHideParamData();\n+   };\n+\n+  $scope.$watch('packet.response_type', responseTypeChanged);\n+  $scope.$watch('packet.command_class', commandClassChanged);\n+\n+  $scope.buildPacket = function() {\n+    $scope.error = '';\n+\n+    var dest_uid = parseUIDOrSetError($scope.packet.dest_uid, 'destination');\n+    if (!dest_uid) {\n+     return;\n+    }\n+\n+    var src_uid = parseUIDOrSetError($scope.packet.src_uid, 'source');\n+    if (!src_uid) {\n+     return;\n+    }\n+\n+    var param_data = [];\n+    if ($scope.packet.command_class === RDM.COMMAND_CLASS.DISCOVERY_COMMAND &&\n+        $scope.packet.param_id === RDM.PIDS.DISC_UNIQUE_BRANCH) {\n+     var lower_uid = parseUIDOrSetError($scope.packet.lower_uid, 'lower');\n+     if (!lower_uid) {\n+      return;\n+     }\n+     var upper_uid = parseUIDOrSetError($scope.packet.upper_uid, 'upper');\n+     if (!upper_uid) {\n+      return;\n+     }\n+     param_data = lower_uid.concat(upper_uid);\n+    } else if (rdmHelperService.isGetSet($scope.packet.command_class)) {\n+     if($scope.packet.response_type === RDM.RESPONSE_TYPE.ACK ||\n+        $scope.packet.response_type === RDM.RESPONSE_TYPE.ACK_OVERFLOW) {\n+      var parse_return = parserService.textToBytes($scope.packet.param_data);\n+      if (parse_return[0]) {\n+       $scope.error = 'Invalid parameter data:' + parse_return[0];\n+       return;\n+      }\n+      param_data = parse_return[1];\n+     } else if ($scope.packet.response_type === RDM.RESPONSE_TYPE.NACK) {\n+      param_data = [$scope.packet.nack_reason >> 8,\n+                    $scope.packet.nack_reason & 0xff];\n+     } else if ($scope.packet.response_type === RDM.RESPONSE_TYPE.ACK_TIMER) {\n+      param_data = [$scope.packet.ack_timer >> 8,\n+                    $scope.packet.ack_timer & 0xff];\n+     }\n+    }\n+\n+    var data = [\n+     $scope.packet.start_code,\n+     $scope.packet.sub_start_code,\n+     param_data.length + 24\n+    ];\n+\n+    data = data.concat(dest_uid);\n+    data = data.concat(src_uid);\n+    data.push($scope.packet.transaction_number);\n+    if (rdmHelperService.isRequest($scope.packet.command_class)) {\n+     data.push($scope.packet.port_id);\n+     data.push(0);  // message count\n+    } else {\n+     data.push($scope.packet.response_type);\n+     data.push($scope.packet.message_count);\n+    }\n+\n+    data.push($scope.packet.sub_device >> 8);\n+    data.push($scope.packet.sub_device & 0xff);\n+    data.push($scope.packet.command_class);\n+    data.push($scope.packet.param_id >> 8);\n+    data.push($scope.packet.param_id & 0xff);\n+\n+    data.push(param_data.length);\n+    data = data.concat(param_data);\n+    data = data.concat(checksumService.checksumAsArray(data));\n+\n+    if ($scope.settings.output_format === OUTPUT_FORMAT.C_ARRAY.value) {\n+     $scope.output = formatService.dataAsArray(data, $scope.settings.wrap);\n+    } else if ($scope.settings.output_format === OUTPUT_FORMAT.C_STRING.value) {\n+     $scope.output = formatService.dataAsString(data, $scope.settings.wrap);\n+    } else if ($scope.settings.output_format === OUTPUT_FORMAT.RAW_HEX.value) {\n+     $scope.output = formatService.dataAsRawHex(data, $scope.settings.wrap);\n+    }\n+   };\n  }])\n \n- .controller('EUIDController', ['$scope', function(convertor) {\n+ .controller('RDMPacketParser',\n+             ['$scope', '$log', 'checksumService', 'parserService',\n+              'formatService', 'rdmHelperService', 'RDM',\n+              function($scope, $log, checksumService, parserService,\n+                       formatService, rdmHelperService, RDM) {\n   'use strict';\n-  convertor.euid = '';\n-  convertor.error = '';\n-  convertor.uid = '';\n+  $scope.packet_data = '';\n+  $scope.show_output = false;\n+\n+  var resetPacket = function() {\n+   $scope.packet = {\n+    'start_code': '',\n+    'sub_start_code': '',\n+    'message_length': '',\n+    'dest_uid': '',\n+    'src_uid': '',\n+    'transaction_number': '',\n+    'port_id': '',\n+    'message_count': '',\n+    'sub_device': '',\n+    'command_class': '',\n+    'param_id': '',\n+    'response_type': '',\n+    'nack_reason': '',\n+    'ack_timer': '',\n+    'param_data_length': '',\n+    'param_data': '',\n+    'checksum': '',\n+    'actual_size': '',\n+    'calculated_checksum': '',\n+    'nack_reason_error': '',\n+    'ack_timer_error': ''\n+   };\n+  };\n+\n+  resetPacket();\n \n-  convertor.convertToUID = function() {\n-   convertor.error = '';\n-   convertor.uid = '';\n+  $scope.reset = function() {\n+   resetPacket();\n+   $scope.error = '';\n+   $scope.packet_data = '';\n+   $scope.show_output = false;\n+  };\n+\n+  $scope.parsePacket = function() {\n+   resetPacket();\n+   $scope.error = '';\n \n-   var clean_euid = convertor.euid.replace(/[,\\s]/g, '').trim();\n-   if (!clean_euid.match(/^[0-9a-fA-F]*$/)) {\n-    convertor.error = 'Invalid EUID: non hex characters';\n+   var parse_return = parserService.textToBytes($scope.packet_data);\n+   if (parse_return[0]) {\n+    $scope.error = 'Invalid packet data:' + parse_return[0];","path":"js_src/rdm.js","position":838,"original_position":838,"commit_id":"88b6de219f4b1fa197a6636f2530bfa176f07a5b","original_commit_id":"88b6de219f4b1fa197a6636f2530bfa176f07a5b","user":{"login":"nomis52","id":4355332,"avatar_url":"https://avatars.githubusercontent.com/u/4355332?v=3","gravatar_id":"","url":"https://api.github.com/users/nomis52","html_url":"https://github.com/nomis52","followers_url":"https://api.github.com/users/nomis52/followers","following_url":"https://api.github.com/users/nomis52/following{/other_user}","gists_url":"https://api.github.com/users/nomis52/gists{/gist_id}","starred_url":"https://api.github.com/users/nomis52/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nomis52/subscriptions","organizations_url":"https://api.github.com/users/nomis52/orgs","repos_url":"https://api.github.com/users/nomis52/repos","events_url":"https://api.github.com/users/nomis52/events{/privacy}","received_events_url":"https://api.github.com/users/nomis52/received_events","type":"User","site_admin":false},"body":"Done","created_at":"2015-06-08T14:42:05Z","updated_at":"2015-06-08T14:42:05Z","html_url":"https://github.com/OpenLightingProject/rdm-app/pull/42#discussion_r31920794","pull_request_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/42","_links":{"self":{"href":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/comments/31920794"},"html":{"href":"https://github.com/OpenLightingProject/rdm-app/pull/42#discussion_r31920794"},"pull_request":{"href":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/42"}}},"pull_request":{"url":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/42","id":36697358,"html_url":"https://github.com/OpenLightingProject/rdm-app/pull/42","diff_url":"https://github.com/OpenLightingProject/rdm-app/pull/42.diff","patch_url":"https://github.com/OpenLightingProject/rdm-app/pull/42.patch","issue_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/issues/42","number":42,"state":"open","locked":false,"title":"Add the RDM packet builder & packet parser.","user":{"login":"nomis52","id":4355332,"avatar_url":"https://avatars.githubusercontent.com/u/4355332?v=3","gravatar_id":"","url":"https://api.github.com/users/nomis52","html_url":"https://github.com/nomis52","followers_url":"https://api.github.com/users/nomis52/followers","following_url":"https://api.github.com/users/nomis52/following{/other_user}","gists_url":"https://api.github.com/users/nomis52/gists{/gist_id}","starred_url":"https://api.github.com/users/nomis52/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nomis52/subscriptions","organizations_url":"https://api.github.com/users/nomis52/orgs","repos_url":"https://api.github.com/users/nomis52/repos","events_url":"https://api.github.com/users/nomis52/events{/privacy}","received_events_url":"https://api.github.com/users/nomis52/received_events","type":"User","site_admin":false},"body":"Demo at http://olp-rdm-staging.appspot.com/tools/packet-extractor\r\n\r\nDave_o: I'm very interested in your comments, esp. how I could structure this better. I'm thinking about moving the generic RDM bits like the constants into the 'RDM' module and then creating separate modules for each tools page. \r\n\r\nAlso some of the code feels clunky, so I'm hoping there are better ways to do it. ","created_at":"2015-06-02T06:11:18Z","updated_at":"2015-06-08T14:42:05Z","closed_at":null,"merged_at":null,"merge_commit_sha":"6660a6e3ddbcea6980740c575f5c87d995515fe8","assignee":{"login":"daveol","id":1355084,"avatar_url":"https://avatars.githubusercontent.com/u/1355084?v=3","gravatar_id":"","url":"https://api.github.com/users/daveol","html_url":"https://github.com/daveol","followers_url":"https://api.github.com/users/daveol/followers","following_url":"https://api.github.com/users/daveol/following{/other_user}","gists_url":"https://api.github.com/users/daveol/gists{/gist_id}","starred_url":"https://api.github.com/users/daveol/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/daveol/subscriptions","organizations_url":"https://api.github.com/users/daveol/orgs","repos_url":"https://api.github.com/users/daveol/repos","events_url":"https://api.github.com/users/daveol/events{/privacy}","received_events_url":"https://api.github.com/users/daveol/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/42/commits","review_comments_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/42/comments","review_comment_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/comments{/number}","comments_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/issues/42/comments","statuses_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/statuses/88b6de219f4b1fa197a6636f2530bfa176f07a5b","head":{"label":"nomis52:packet-builder","ref":"packet-builder","sha":"88b6de219f4b1fa197a6636f2530bfa176f07a5b","user":{"login":"nomis52","id":4355332,"avatar_url":"https://avatars.githubusercontent.com/u/4355332?v=3","gravatar_id":"","url":"https://api.github.com/users/nomis52","html_url":"https://github.com/nomis52","followers_url":"https://api.github.com/users/nomis52/followers","following_url":"https://api.github.com/users/nomis52/following{/other_user}","gists_url":"https://api.github.com/users/nomis52/gists{/gist_id}","starred_url":"https://api.github.com/users/nomis52/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nomis52/subscriptions","organizations_url":"https://api.github.com/users/nomis52/orgs","repos_url":"https://api.github.com/users/nomis52/repos","events_url":"https://api.github.com/users/nomis52/events{/privacy}","received_events_url":"https://api.github.com/users/nomis52/received_events","type":"User","site_admin":false},"repo":{"id":34217967,"name":"rdm-app","full_name":"nomis52/rdm-app","owner":{"login":"nomis52","id":4355332,"avatar_url":"https://avatars.githubusercontent.com/u/4355332?v=3","gravatar_id":"","url":"https://api.github.com/users/nomis52","html_url":"https://github.com/nomis52","followers_url":"https://api.github.com/users/nomis52/followers","following_url":"https://api.github.com/users/nomis52/following{/other_user}","gists_url":"https://api.github.com/users/nomis52/gists{/gist_id}","starred_url":"https://api.github.com/users/nomis52/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nomis52/subscriptions","organizations_url":"https://api.github.com/users/nomis52/orgs","repos_url":"https://api.github.com/users/nomis52/repos","events_url":"https://api.github.com/users/nomis52/events{/privacy}","received_events_url":"https://api.github.com/users/nomis52/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/nomis52/rdm-app","description":"Code for the rdm.openlighting.org site","fork":true,"url":"https://api.github.com/repos/nomis52/rdm-app","forks_url":"https://api.github.com/repos/nomis52/rdm-app/forks","keys_url":"https://api.github.com/repos/nomis52/rdm-app/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nomis52/rdm-app/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nomis52/rdm-app/teams","hooks_url":"https://api.github.com/repos/nomis52/rdm-app/hooks","issue_events_url":"https://api.github.com/repos/nomis52/rdm-app/issues/events{/number}","events_url":"https://api.github.com/repos/nomis52/rdm-app/events","assignees_url":"https://api.github.com/repos/nomis52/rdm-app/assignees{/user}","branches_url":"https://api.github.com/repos/nomis52/rdm-app/branches{/branch}","tags_url":"https://api.github.com/repos/nomis52/rdm-app/tags","blobs_url":"https://api.github.com/repos/nomis52/rdm-app/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nomis52/rdm-app/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nomis52/rdm-app/git/refs{/sha}","trees_url":"https://api.github.com/repos/nomis52/rdm-app/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nomis52/rdm-app/statuses/{sha}","languages_url":"https://api.github.com/repos/nomis52/rdm-app/languages","stargazers_url":"https://api.github.com/repos/nomis52/rdm-app/stargazers","contributors_url":"https://api.github.com/repos/nomis52/rdm-app/contributors","subscribers_url":"https://api.github.com/repos/nomis52/rdm-app/subscribers","subscription_url":"https://api.github.com/repos/nomis52/rdm-app/subscription","commits_url":"https://api.github.com/repos/nomis52/rdm-app/commits{/sha}","git_commits_url":"https://api.github.com/repos/nomis52/rdm-app/git/commits{/sha}","comments_url":"https://api.github.com/repos/nomis52/rdm-app/comments{/number}","issue_comment_url":"https://api.github.com/repos/nomis52/rdm-app/issues/comments{/number}","contents_url":"https://api.github.com/repos/nomis52/rdm-app/contents/{+path}","compare_url":"https://api.github.com/repos/nomis52/rdm-app/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nomis52/rdm-app/merges","archive_url":"https://api.github.com/repos/nomis52/rdm-app/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nomis52/rdm-app/downloads","issues_url":"https://api.github.com/repos/nomis52/rdm-app/issues{/number}","pulls_url":"https://api.github.com/repos/nomis52/rdm-app/pulls{/number}","milestones_url":"https://api.github.com/repos/nomis52/rdm-app/milestones{/number}","notifications_url":"https://api.github.com/repos/nomis52/rdm-app/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nomis52/rdm-app/labels{/name}","releases_url":"https://api.github.com/repos/nomis52/rdm-app/releases{/id}","created_at":"2015-04-19T18:08:31Z","updated_at":"2015-05-07T10:26:27Z","pushed_at":"2015-06-05T21:57:42Z","git_url":"git://github.com/nomis52/rdm-app.git","ssh_url":"git@github.com:nomis52/rdm-app.git","clone_url":"https://github.com/nomis52/rdm-app.git","svn_url":"https://github.com/nomis52/rdm-app","homepage":"http://rdm.openlighting.org","size":1191,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"OpenLightingProject:master","ref":"master","sha":"d76e87fa50425fa011bc52fd38c59068e0a48461","user":{"login":"OpenLightingProject","id":6199314,"avatar_url":"https://avatars.githubusercontent.com/u/6199314?v=3","gravatar_id":"","url":"https://api.github.com/users/OpenLightingProject","html_url":"https://github.com/OpenLightingProject","followers_url":"https://api.github.com/users/OpenLightingProject/followers","following_url":"https://api.github.com/users/OpenLightingProject/following{/other_user}","gists_url":"https://api.github.com/users/OpenLightingProject/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenLightingProject/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenLightingProject/subscriptions","organizations_url":"https://api.github.com/users/OpenLightingProject/orgs","repos_url":"https://api.github.com/users/OpenLightingProject/repos","events_url":"https://api.github.com/users/OpenLightingProject/events{/privacy}","received_events_url":"https://api.github.com/users/OpenLightingProject/received_events","type":"Organization","site_admin":false},"repo":{"id":15259823,"name":"rdm-app","full_name":"OpenLightingProject/rdm-app","owner":{"login":"OpenLightingProject","id":6199314,"avatar_url":"https://avatars.githubusercontent.com/u/6199314?v=3","gravatar_id":"","url":"https://api.github.com/users/OpenLightingProject","html_url":"https://github.com/OpenLightingProject","followers_url":"https://api.github.com/users/OpenLightingProject/followers","following_url":"https://api.github.com/users/OpenLightingProject/following{/other_user}","gists_url":"https://api.github.com/users/OpenLightingProject/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenLightingProject/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenLightingProject/subscriptions","organizations_url":"https://api.github.com/users/OpenLightingProject/orgs","repos_url":"https://api.github.com/users/OpenLightingProject/repos","events_url":"https://api.github.com/users/OpenLightingProject/events{/privacy}","received_events_url":"https://api.github.com/users/OpenLightingProject/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/OpenLightingProject/rdm-app","description":"Code for the rdm.openlighting.org site","fork":false,"url":"https://api.github.com/repos/OpenLightingProject/rdm-app","forks_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/forks","keys_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/teams","hooks_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/hooks","issue_events_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/issues/events{/number}","events_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/events","assignees_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/assignees{/user}","branches_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/branches{/branch}","tags_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/tags","blobs_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/languages","stargazers_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/stargazers","contributors_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/contributors","subscribers_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/subscribers","subscription_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/subscription","commits_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/contents/{+path}","compare_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/merges","archive_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/downloads","issues_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/issues{/number}","pulls_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/labels{/name}","releases_url":"https://api.github.com/repos/OpenLightingProject/rdm-app/releases{/id}","created_at":"2013-12-17T16:33:19Z","updated_at":"2015-05-07T10:26:56Z","pushed_at":"2015-06-05T21:57:42Z","git_url":"git://github.com/OpenLightingProject/rdm-app.git","ssh_url":"git@github.com:OpenLightingProject/rdm-app.git","clone_url":"https://github.com/OpenLightingProject/rdm-app.git","svn_url":"https://github.com/OpenLightingProject/rdm-app","homepage":"http://rdm.openlighting.org","size":1544,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":4,"mirror_url":null,"open_issues_count":7,"forks":4,"open_issues":7,"watchers":2,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/42"},"html":{"href":"https://github.com/OpenLightingProject/rdm-app/pull/42"},"issue":{"href":"https://api.github.com/repos/OpenLightingProject/rdm-app/issues/42"},"comments":{"href":"https://api.github.com/repos/OpenLightingProject/rdm-app/issues/42/comments"},"review_comments":{"href":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/42/comments"},"review_comment":{"href":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/OpenLightingProject/rdm-app/pulls/42/commits"},"statuses":{"href":"https://api.github.com/repos/OpenLightingProject/rdm-app/statuses/88b6de219f4b1fa197a6636f2530bfa176f07a5b"}}}}