{
  "AutomaticReconnect": true,
  "SerialPortName": "",
  "StartupDelayMs": 0,
  "IsConnecting": false,
  "IsEnabled": true,
  "LogIncomingData": false,
  "IsConnected": false,
  "BaudRate": 115200,
  "UpdateMessages": [
    {
      "Message": {
        "Interpreter": 1,
        "Expression": "// MUTAR GF-1 - mensaje de telemetria para SimHub (Custom serial devices, JavaScript)\n// Formato: $D,<marcha>,<rpm x10>,<cambio x10>,<bandera 0-9>,<ultima vuelta ms>,<mejor 0/1>,<juego 0/1>\\n\n//          $E,... datos de las vistas de la pantalla izquierda (firmware 1.1 o mas nuevo; los anteriores la ignoran)\n//          $R,... relative: 2 autos adelante, vos y 2 atras (firmware 1.2 o mas nuevo)\nfunction num(v) {\n  if (v === null || v === undefined) return 0;\n  if (typeof v === 'number') return v;\n  if (typeof v === 'boolean') return v ? 1 : 0;\n  var n = parseFloat(v);\n  return isNaN(n) ? 0 : n;\n}\nfunction lapMs(v) {\n  if (v === null || v === undefined) return 0;\n  if (typeof v === 'number') return Math.round(v * 1000);\n  if (v.TotalMilliseconds !== undefined) return Math.round(num(v.TotalMilliseconds));\n  var s = String(v);\n  var m = s.match(/^(?:(\\d+)\\.)?(\\d+):(\\d+):(\\d+(?:\\.\\d+)?)$/);\n  if (m) return Math.round(((m[1] ? +m[1] : 0) * 86400 + (+m[2]) * 3600 + (+m[3]) * 60 + parseFloat(m[4])) * 1000);\n  return 0;\n}\nfunction flagOn(n) { return num($prop('DataCorePlugin.GameData.Flag_' + n)) > 0; }\n\nvar g = $prop('DataCorePlugin.GameData.Gear');\ng = (g === null || g === undefined || String(g) === '') ? 'N' : String(g).charAt(0).toUpperCase();\nvar rpm = num($prop('DataCorePlugin.GameData.CarSettings_CurrentDisplayedRPMPercent'));\nvar shift = num($prop('DataCorePlugin.GameData.CarSettings_RedLineDisplayedPercent'));\nif (shift < 50 || shift > 100) shift = num($prop('Settings.shift')) || 96;\n\nvar fl = 0;\nvar sf = $prop('DataCorePlugin.GameRawData.Telemetry.SessionFlags');\nif (sf !== null && sf !== undefined) {           // iRacing: banderas completas\n  sf = num(sf);\n  if (sf & 0x20000) fl = 8;                        // descalificado\n  else if (sf & 0x10000) fl = 6;                   // negra\n  else if (sf & 0x100000) fl = 7;                  // meatball (reparar)\n  else if (sf & 0x1) fl = 5;                       // a cuadros\n  else if (sf & (0x8 | 0x100 | 0x4000 | 0x8000)) fl = 2; // amarilla\n  else if (sf & 0x40) fl = 9;                      // pista sucia / objetos en pista (debris)\n  else if (sf & 0x20) fl = 3;                      // azul\n  else if (sf & 0x2) fl = 4;                       // blanca\n  else if (sf & 0x4) fl = 1;                       // verde\n} else {                                          // resto de los juegos\n  if (flagOn('Black')) fl = 6;\n  else if (flagOn('Orange')) fl = 7;\n  else if (flagOn('Checkered')) fl = 5;\n  else if (flagOn('Yellow')) fl = 2;\n  else if (flagOn('Blue')) fl = 3;\n  else if (flagOn('White')) fl = 4;\n  else if (flagOn('Green')) fl = 1;\n}\n\nvar last = lapMs($prop('DataCorePlugin.GameData.LastLapTime'));\nvar bestLap = lapMs($prop('DataCorePlugin.GameData.BestLapTime'));\nvar best = (last > 0 && bestLap > 0 && Math.abs(last - bestLap) <= 1) ? 1 : 0;\nvar run = num($prop('DataCorePlugin.GameRunning')) > 0 ? 1 : 0;\nif (!run) fl = 0;                                 // juego cerrado: SimHub puede quedarse con la ultima bandera\n\nvar lineD = '$D,' + g + ',' + Math.round(rpm * 10) + ',' + Math.round(shift * 10) + ',' + fl + ',' + last + ',' + best + ',' + run + '\\n';\n\n// ---------------------------------------------------------------- vistas ($E)\nfunction gd(n) { return $prop('DataCorePlugin.GameData.' + n); }\nfunction r(v) { return Math.round(num(v)); }\nfunction has(v) { return v !== null && v !== undefined && String(v) !== ''; }\n\nvar spUnit = String(gd('SpeedLocalUnit') || '').toLowerCase().indexOf('mph') >= 0 ? 1 : 0;\n\n// delta contra la mejor vuelta de la sesion (segundos). iRacing lo da por su cuenta.\nvar dl = gd('DeltaToSessionBest');\nif (!has(dl)) {\n  var ok = $prop('DataCorePlugin.GameRawData.Telemetry.LapDeltaToSessionBestLap_OK');\n  if (has(ok) && num(ok) > 0) dl = $prop('DataCorePlugin.GameRawData.Telemetry.LapDeltaToSessionBestLap');\n}\nif (!has(dl)) dl = $prop('PersistantTrackerPlugin.SessionBestLiveDeltaSeconds');\nvar delta = has(dl) && Math.abs(num(dl)) < 100 ? Math.round(num(dl) * 1000) : 'x';\n\nvar totalLaps = r(gd('TotalLaps'));\nvar left = gd('SessionTimeLeft');\nvar timeLeft = has(left) ? Math.round(lapMs(left) / 1000) : -1;\nif (timeLeft < 0 || timeLeft > 359999) timeLeft = -1;\n\nvar fuelLaps = gd('EstimatedFuelRemaingLaps');\nif (!has(fuelLaps)) fuelLaps = $prop('DataCorePlugin.Computed.Fuel_RemainingLaps');\n// 0 = SimHub todavia no lo calculo (antes de completar una vuelta): se manda como \"sin dato\"\nfuelLaps = has(fuelLaps) && num(fuelLaps) > 0 && num(fuelLaps) < 1000 ? Math.round(num(fuelLaps) * 10) : -1;\nvar gal = String(gd('FuelUnit') || '').toLowerCase().indexOf('gal') >= 0 ? 1 : 0;\nvar fahr = String(gd('TemperatureUnit') || '').toUpperCase().indexOf('F') >= 0 ? 1 : 0;\n\n// trazado: coordenadas del auto si el juego las da (x, z = plano del piso); si no, el GF-1 lo arma con velocidad y orientacion\nvar hasXY = 0, cx = 0, cz = 0;\ntry {\n  var c = gd('CarCoordinates');\n  if (c && (c.length >= 3 || c.Length >= 3)) {\n    cx = num(c[0]); cz = num(c[2]);\n    if (cx !== 0 || cz !== 0) hasXY = 1;\n  }\n} catch (e) { hasXY = 0; }\n\n// identificador corto del circuito (juego + pista + variante)\nvar tid = String($prop('DataCorePlugin.CurrentGame') || '') + '|' + String(gd('TrackIdWithConfig') || gd('TrackId') || gd('TrackName') || '');\nvar th = 0;\nfor (var i = 0; i < tid.length; i++) th = (th * 31 + tid.charCodeAt(i)) & 0xFFFF;\n\nvar lineE = '$E,' + [\n  r(gd('SpeedLocal')), spUnit, r(gd('SpeedKmh')), delta,\n  lapMs(gd('CurrentLapTime')), lapMs(gd('BestLapTime')),\n  r(gd('Position')), r(gd('OpponentsCount')), r(gd('CurrentLap')), totalLaps, timeLeft,\n  Math.round(num(gd('Fuel')) * 10), fuelLaps, r(gd('FuelPercent')), gal,\n  r(gd('TyreTemperatureFrontLeft')), r(gd('TyreTemperatureFrontRight')), r(gd('TyreTemperatureRearLeft')), r(gd('TyreTemperatureRearRight')), fahr,\n  r(gd('Throttle')), r(gd('Brake')), r(gd('Clutch')),\n  Math.round(num(gd('BrakeBias')) * 10), r(gd('TCLevel')), r(gd('ABSLevel')),\n  r(gd('WaterTemperature')), r(gd('OilTemperature')),\n  Math.round(num(gd('TrackPositionPercent')) * 10000), hasXY, Math.round(cx * 10), Math.round(cz * 10),\n  Math.round(num(gd('OrientationYaw')) * 10), th,\n  num(gd('IsInPitLane')) > 0 ? 1 : 0, num(gd('PitLimiterOn')) > 0 ? 1 : 0,\n  // latido: SimHub solo manda cuando el mensaje cambia; con esto cambia 2 veces por segundo aunque\n  // el juego este quieto (pausa, boxes) y el GF-1 no pasa a \"Esperando SimHub\". El firmware lo ignora.\n  Math.floor(Date.now() / 500) % 100\n].join(',') + '\\n';\n\n// ---------------------------------------------------------------- relative ($R, firmware 1.2; los anteriores la ignoran)\n// $R,<gomas 0/1>, y 5 grupos <pos>,<etiqueta>,<gap x10 s>,<goma>,<vuelta>: adelante2, adelante1, vos, atras1, atras2\n// ('x' = sin dato). Etiqueta: numero de auto, o las iniciales del piloto si el juego no da numero (AC).\n// Gap negativo = va adelante tuyo. Goma: 0 no se sabe, 1 blanda, 2 media, 3 dura / seco, 4 intermedia, 5 lluvia.\n// Vuelta (solo en carrera): 1 = va una vuelta adelante tuyo (atras en pista = te esta doblando), -1 = una atras, 0 = igual.\n// SimHub informa la goma como letras (S, M, H, I, W...) o con el nombre del juego.\nfunction tyreCode(c) {\n  if (!has(c)) return 0;\n  var s = String(c).toUpperCase();\n  if (s.indexOf('INT') === 0 || s === 'I') return 4;\n  if (s.charAt(0) === 'W' || s.indexOf('RAIN') >= 0 || s.indexOf('WET') >= 0) return 5;\n  if (s.charAt(0) === 'S' || s.charAt(0) === 'U') return 1;   // blanda, super / ultra blanda\n  if (s.charAt(0) === 'M') return 2;\n  if (s.charAt(0) === 'H' || s.charAt(0) === 'D') return 3;   // dura, \"dry\" (goma de seco unica)\n  return 0;\n}\nfunction clean(v, max) { return has(v) ? String(v).toUpperCase().replace(/[^A-Z0-9]/g, '').substring(0, max) : ''; }\n// Iniciales: nombre y apellido (\"Max Verstappen\" -> \"MV\")\nfunction initials(name) {\n  if (!has(name)) return '';\n  var w = String(name).replace(/[^A-Za-zÀ-ÿ ]/g, ' ').trim().split(/\\s+/);\n  return clean(w.length > 1 ? w[0].charAt(0) + w[w.length - 1].charAt(0) : w[0].substring(0, 2), 2);\n}\n// Etiqueta de cada auto: el numero si el juego lo da; si no, las iniciales del piloto\nfunction tagOf(number, ini, name) { return clean(number, 3) || clean(ini, 2) || initials(name); }\nfunction call(f, a) { try { return f ? f(a) : null; } catch (e) { return null; } }\n// funciones de SimHub por posicion (null si esta version de SimHub no la tiene)\nvar F_GAP = typeof driverrelativegaptoplayer === 'function' ? driverrelativegaptoplayer : null;\nvar F_TYRE = typeof driverfronttyrecompound === 'function' ? driverfronttyrecompound : null;\nvar F_NUM = typeof drivercarnumber === 'function' ? drivercarnumber : null;\nvar F_INI = typeof driverinitials === 'function' ? driverinitials : null;\nvar F_NAME = typeof drivername === 'function' ? drivername : null;\nvar isRace = String(gd('SessionTypeName') || '').toUpperCase().indexOf('RACE') >= 0;\n// Segundos de una diferencia: SimHub la puede dar como numero, como TimeSpan o como texto \"00:00:01.4000000\"\nfunction secs(v) {\n  if (!has(v)) return null;\n  if (typeof v === 'number') return v;\n  if (v.TotalSeconds !== undefined) return num(v.TotalSeconds);\n  var s = String(v).trim(), neg = s.charAt(0) === '-';\n  if (neg) s = s.substring(1);\n  if (s.indexOf(':') >= 0) { var ms = lapMs(s); return ms ? (neg ? -ms : ms) / 1000 : (/[1-9]/.test(s) ? null : 0); }\n  var n = parseFloat(s);\n  return isNaN(n) ? null : (neg ? -n : n);\n}\n// Primera diferencia que sirva (0 en un rival no es real: se prueba la siguiente). Sin ninguna: 'x' (no se muestra)\nfunction gap10(list, ahead) {\n  for (var i = 0; i < list.length; i++) {\n    var g = secs(list[i]);\n    if (g !== null && g !== 0 && Math.abs(g) < 1000) return Math.max(1, Math.round(Math.abs(g) * 10)) * (ahead ? -1 : 1);\n  }\n  return 'x';\n}\nfunction pt(side, i, f) { return $prop('PersistantTrackerPlugin.Driver' + side + '_0' + i + '_' + f); }\nfunction ptGaps(k) {   // diferencias del seguimiento de SimHub para el auto k (-2..2) en pista\n  var side = k < 0 ? 'Ahead' : 'Behind', ix = Math.abs(k) - 1;\n  return [pt(side, ix, 'RelativeGapToPlayer'), pt(side, ix, 'Gap'), pt(side, ix, 'GapToPlayer')];\n}\n\nvar lineR = '';\ntry {\n  var rows = [], tyres = 0, okR = false;\n  if (typeof getopponentleaderboardposition_aheadbehind === 'function') {\n    // funciones de SimHub por posicion: traen numero, diferencia en pista y goma de cada auto\n    var myPos = num(getopponentleaderboardposition_aheadbehind(0));\n    for (var k = -2; k <= 2; k++) {\n      var lp = num(getopponentleaderboardposition_aheadbehind(k));\n      if (lp <= 0) { rows.push('x,,x,x,0'); continue; }\n      okR = true;\n      var tc = tyreCode(call(F_TYRE, lp));\n      if (tc) tyres = 1;\n      var tag = tagOf(call(F_NUM, lp), call(F_INI, lp), call(F_NAME, lp));\n      // en carrera: atras en pista pero mejor posicion = te esta doblando; adelante con peor posicion = lo estas doblando\n      var lap = !isRace || myPos <= 0 || k === 0 ? 0 : (k > 0 && lp < myPos) ? 1 : (k < 0 && lp > myPos) ? -1 : 0;\n      rows.push([lp, tag, k === 0 ? 0 : gap10([call(F_GAP, lp)].concat(ptGaps(k)), k < 0), tc, lap].join(','));\n    }\n  }\n  if (!okR) {\n    // SimHub mas viejo: autos de adelante y atras del seguimiento de SimHub (sin goma)\n    rows = [];\n    var spec = [['Ahead', 1], ['Ahead', 0], null, ['Behind', 0], ['Behind', 1]];\n    for (var j = 0; j < 5; j++) {\n      if (!spec[j]) { rows.push([r(gd('Position')) || 'x', '', 0, 0, 0].join(',')); continue; }\n      var sd = spec[j][0], ix = spec[j][1], ps = r(pt(sd, ix, 'Position'));\n      if (ps <= 0) { rows.push('x,,x,x,0'); continue; }\n      okR = true;\n      var my = r(gd('Position')), ahead = sd === 'Ahead';\n      var lp2 = !isRace || my <= 0 ? 0 : (!ahead && ps < my) ? 1 : (ahead && ps > my) ? -1 : 0;\n      rows.push([ps, tagOf(pt(sd, ix, 'CarNumber'), pt(sd, ix, 'Initials'), pt(sd, ix, 'Name')),\n                 gap10(ptGaps(ahead ? -1 - ix : 1 + ix), ahead), 0, lp2].join(','));\n    }\n  }\n  if (okR) lineR = '$R,' + tyres + ',' + rows.join(',') + '\\n';\n} catch (e) { lineR = ''; var relErr = String(e && e.message); }   // si algo falla, la vista Relative dice \"Sin datos\" y el resto sigue andando\n\nreturn lineD + lineE + lineR;\n"
      },
      "IsEnabled": true,
      "MaximumFrequency": 60
    }
  ],
  "OnConnectMessage": {
    "Interpreter": 1,
    "Expression": "return '$H\\n';"
  },
  "OnDisconnectMessage": {
    "Interpreter": 1,
    "Expression": "return '$X\\n';"
  },
  "DtrEnable": true,
  "RtsEnable": false,
  "EditorExpanded": false,
  "Name": "MUTAR GF-1",
  "Description": "MUTAR SIMRACING - Modulo de marcha, banderas, RPM y vistas (v1.1). Elegí el puerto COM del GF-1.",
  "LastErrorDate": null,
  "LastErrorMessage": null,
  "IsFreezed": false,
  "SettingsBuilder": {
    "Settings": [
      {
        "Maximum": 100,
        "Minimum": 85,
        "PropertyName": "shift",
        "CurrentValue": 96,
        "Name": null,
        "TypeName": "SliderEntry",
        "IsEnabled": true,
        "IsEnabledFormula": {
          "Expression": ""
        },
        "IsVisible": true,
        "Label": "Punto de cambio (%) si el auto no lo informa",
        "IsVisibleFormula": {
          "Expression": ""
        }
      }
    ],
    "IsEditMode": false
  }
}