symbol) >= 1 && strlen($this->symbolTable) >= 1) { $symbolAsciiValue = ord(substr($this->symbol, 0, 1)); $symbolTableAsciiValue = ord(substr($this->symbolTable, 0, 1)); } else { // Default values $symbolAsciiValue = 125; $symbolTableAsciiValue = 47; } $scaleStrValue = ''; if ($scaleWidth !== null && $scaleHeight !== null) { $scaleStrValue = '-scale' . $scaleWidth . 'x' . $scaleHeight; } return '/symbols/symbol-' . $symbolAsciiValue . '-' . $symbolTableAsciiValue . $scaleStrValue . '.svg'; } /** * Get PGH range in meters * * @return float */ public function getPHGRange() { if ($this->getPhg() != null) { $p = $this->getPhgPower(); $h = $this->getPhgHaat(false); $g = $this->getPhgGain(); $gain = pow(10, ($g/10)); //converts from DB to decimal $range = sqrt(2 * $h * sqrt(($p / 10) * ($gain / 2))); return $range / 0.000621371192; // convert to m and return } return null; } /** * Get PGH description * * @return String */ public function getPHGDescription() { if ($this->getPhg() != null) { $power = $this->getPhgPower(); $haat = $this->getPhgHaat(); $gain = $this->getPhgGain(); $direction = $this->getPhgDirection(); $range = $this->getPHGRange(); $description = ''; if ($power !== null) { $description .= 'Power ' . $power . ' W'; } if ($haat !== null) { if (strlen($description) > 0) { $description .= ', '; } if (isImperialUnitUser()) { $description .= 'Height ' . round(convertMeterToFeet($haat), 0) . ' ft'; } else { $description .= 'Height ' . $haat . ' m'; } } if ($gain !== null && $direction !== null) { if (strlen($description) > 0) { $description .= ', '; } $description .= 'Gain ' . $gain . ' dB ' . $direction; } return $description; } return null; } /** * Get PGH string * * @return string */ public function getPhg() { if ($this->phg != null) { if ($this->phg == 0) { return null; // 0000 is considered not to be used (power == 0!) } else if ($this->phg < 10) { return '000' + strval($this->phg); } else if ($this->phg < 100) { return '00' + strval($this->phg); } else if ($this->phg < 1000) { return '0' + strval($this->phg); } else { return strval($this->phg); } } return null; } /** * Get PGH power * * @return int */ public function getPhgPower() { if ($this->getPhg() != null) { return pow(intval(substr($this->getPhg(), 0, 1)), 2); } return null; } /** * Get PGH hight (above averange terrain) * * @param boolean $inMeters * @return int */ public function getPhgHaat($inMeters = true) { if ($this->getPhg() != null) { $value = intval(substr($this->getPhg(), 1, 1)); $haat = 0; if ($value != 0) { $haat = 10 * pow(2, $value); } if ($inMeters) { return intval(round($haat * 0.3048)); } else { return $haat; } } return null; } /** * Get PGH Gain * * @return int */ public function getPhgGain() { if ($this->getPhg() != null) { return intval(substr($this->getPhg(), 2, 1)); } return null; } /** * Get PGH Direction * * @return String */ public function getPhgDirection() { if ($this->getPhg() != null) { switch (substr($this->getPhg(), 3, 1)) { case 0: return 'omni'; break; case 1: return 'North East'; break; case 2: return 'East'; break; case 3: return 'South East'; break; case 4: return 'South'; break; case 5: return 'South West'; break; case 6: return 'West'; break; case 7: return 'North West'; break; case 8: return 'North'; break; case 9: return null; break; } } return null; } /** * Get PGH Direction Degree * * @return int */ public function getPhgDirectionDegree() { if ($this->getPhg() != null) { switch (substr($this->getPhg(), 3, 1)) { case 0: return null; break; case 1: return 45; break; case 2: return 90; break; case 3: return 135; break; case 4: return 180; break; case 5: return 225; break; case 6: return 270; break; case 7: return 315; break; case 8: return 360; break; case 9: return null; break; } } return null; } /** * Get RNG * * @return float */ public function getRng() { if ($this->rng != null) { return $this->rng; } return null; } /** * Get packet type description * * @return Station */ public function getPacketTypeName() { switch ($this->packetTypeId) { case 1: return 'Position'; break; case 2: return 'Direction'; break; case 3: return 'Weather'; break; case 4: return 'Object'; break; case 5: return 'Item'; break; case 6: return 'Telemetry'; break; case 7: return 'Message'; break; case 8: return 'Query'; break; case 9: return 'Response'; break; case 10: return 'Status'; break; case 11: return 'Other'; break; case 12: return 'Unknown'; break; case 13: return 'Invalid'; break; case 14: return 'Capability'; break; default: return 'Unknown'; break; } } /** * Get releted packet weather * @return PacketWeather */ public function getPacketWeather() { return PacketWeatherRepository::getInstance()->getObjectByPacketId($this->id, $this->timestamp); } /** * Get releted packet telemetry * @return PacketTelemetry */ public function getPacketTelemetry() { return PacketTelemetryRepository::getInstance()->getObjectByPacketId($this->id, $this->timestamp); } /** * Returns OGN part of packet * * @return PacketOgn */ public function getPacketOgn() { static $cache = array(); $key = $this->id; if (!isset($cache[$key])) { if ($this->sourceId == 5) { $cache[$key] = PacketOgnRepository::getInstance()->getObjectByPacketId($this->id, $this->timestamp); } else { $cache[$key] = new PacketOgn(null); } } return $cache[$key]; } /** * Get Station * @return Station */ public function getStationObject() { return StationRepository::getInstance()->getObjectById($this->stationId); } /** * Get Sender * @return Sender */ public function getSenderObject() { return SenderRepository::getInstance()->getObjectById($this->senderId); } /** * Get Receiver * @return Receiver */ public function getReceiverObject() { return ReceiverRepository::getInstance()->getObjectById($this->receiverId); } /* const qAC = 'received from the client directly via a verified connection'; const qAU = 'received from the client directly via a UDP connection'; const qAO = 'received via a client-only port and the FROMCALL does not match the login'; const qAS = 'received from another server or generated by this server'; const qAR = 'received directly (via a verified connection) from an IGate using the ,I construct'; */ /** * Get packet path quality based on digipeater hops used * @return int $quality (0-4, 0 = unknown, 2 = 3 hops, 3 = 2 hops, 4+ = 1 hop) */ public function getPathInfo() { if ($this->rawPath == '') return null; $path = explode(',', $this->rawPath); // A valid path cannot have less than 3 elements if (sizeof($path) < 3) return null; // To call $to_call = array_shift($path); // Final destination $last_hop = array_pop($path); $gateway = array_pop($path); // Iterate remaining path elements $info = ['quality' => 4, 'hops' => 1, 'type' => 'Unknown', 'igate' => false]; foreach ($path AS $p) { // Digi hop? if (strpos($p, 'WIDE') === false && strpos($p, 'RELAY') === false && strpos($p, 'TRACE') === false && strpos($p, 'TCPIP') === false && strpos($p, 'TCPXX') === false && strpos($p, 'DMR*') === false && $p != $last_hop) { if ($this->source_id != SOURCE_CWOP) { $info['quality']--; } $info['type'] = "RF via Digi/iGate ($last_hop)"; $info['hops']++; } else if (strpos($p, 'TCPIP') !== false) { $info['igate'] = true; } else if (strpos($p, 'DMR') !== false) { $info['type'] = "RF via DMR ($last_hop)"; } } if (strpos($gateway, 'qAC') !== false) $info['type'] = 'APRS-IS Direct, Verified Login'; else if (strpos($gateway, 'qAU') !== false) $info['type'] = 'APRS-IS IS Direct via UDP'; else if (strpos($gateway, 'qAO') !== false) $info['type'] = 'APRS-IS Direct via Client Port'; else if (strpos($gateway, 'qAS') !== false) $info['type'] = 'Forwarded via APRS-IS'; else if (strpos($gateway, 'qAR') !== false && $info['type'] == 'Unknown') $info['type'] = 'RF Direct'; else if (strpos($gateway, 'qAX') !== false) $info['type'] = 'APRS-IS Direct, Unverified Login'; return $info; } /** * Get packet equipment type name based on to_call * * @return String */ public function getEquipmentTypeName() { $path = explode(',', $this->rawPath ?? ''); if (sizeof($path) > 1) { $path = explode('-', $path[0]); $pdo = PDOConnection::getInstance(); $sql = 'SELECT e.*, c.description, c.display_name FROM equipment e LEFT JOIN equipment_class c ON (c.id = e.class_id) WHERE e.to_call = ? OR e.to_call = ? OR e.to_call = ? ORDER BY e.to_call DESC LIMIT 1'; $stmt = $pdo->prepareAndExec($sql, [$path[0], substr($path[0], 0, 5).'*', substr($path[0], 0, 4) . '**']); $record1 = $stmt->fetch(PDO::FETCH_ASSOC); // Extended Description if (!empty($record1) && is_array($record1) && !is_null($record1['model'])) { $sql = 'SELECT description AS extended_description FROM to_calls WHERE callsign = ? OR callsign = ? OR callsign = ? ORDER BY callsign DESC LIMIT 1'; $stmt = $pdo->prepareAndExec($sql, [$path[0], substr($path[0], 0, 5).'*', substr($path[0], 0, 4) . '**']); $record2 = $stmt->fetch(PDO::FETCH_ASSOC); return is_array($record2) ? array_merge($record1, $record2) : $record1; } else if (is_array($record1)) return $record1; } return null; } public $MicEData = array(); // Ref: http://www.aprs.org/aprs12/mic-e-types.txt const radioTypes = [ 'knownPrefixes' => ['>', ']', '`', '\''], 'radios' => [ [ 'prefix' => '>', 'suffix' => 'v', 'name' => 'Kenwood TH-D7A Mobile' ], [ 'prefix' => ']', 'suffix' => '', 'name' => 'Kenwood TM-D700 Mobile' ], [ 'prefix' => ']', 'suffix' => '=', 'name' => 'Kenwood TM-D710 Mobile' ], [ 'prefix' => '>', 'suffix' => '=', 'name' => 'Kenwood TH-D72 Handheld' ], [ 'prefix' => '>', 'suffix' => '^', 'name' => 'Kenwood TH-D74 Handheld' ], [ 'prefix' => '>', 'suffix' => '&', 'name' => 'Kenwood TH-D75 Handheld' ], [ 'prefix' => '`', 'suffix' => '_ ', 'name' => 'SQ8L VP-Tracker' ], [ 'prefix' => '`', 'suffix' => '_ ', 'name' => 'Yaesu VX-8 Handheld' ], [ 'prefix' => '`', 'suffix' => '_#', 'name' => 'Yaesu VX-8G Handheld' ], [ 'prefix' => '`', 'suffix' => '_$', 'name' => 'Yaesu FT1D Handheld' ], [ 'prefix' => '`', 'suffix' => '_(', 'name' => 'Yaesu FT2D Handheld' ], [ 'prefix' => '`', 'suffix' => '_0', 'name' => 'Yaesu FT3D Handheld' ], [ 'prefix' => '`', 'suffix' => '_3', 'name' => 'Yaesu FT5D Handheld' ], [ 'prefix' => '`', 'suffix' => '_)', 'name' => 'Yaesu FTM-100D Mobile' ], [ 'prefix' => '`', 'suffix' => '_"', 'name' => 'Yaesu FTM-350 Mobile' ], [ 'prefix' => '`', 'suffix' => '_2', 'name' => 'Yaesu FTM-200DR Mobile' ], [ 'prefix' => '`', 'suffix' => '_1', 'name' => 'Yaesu FTM-300DR Mobile' ], [ 'prefix' => '`', 'suffix' => '_%', 'name' => 'Yaesu FTM-400DR Mobile' ], [ 'prefix' => '`', 'suffix' => '_4', 'name' => 'Yaesu FTM-500DR Mobile' ], [ 'prefix' => '\'', 'suffix' => '|3', 'name' => 'Byonics TinyTrack3' ], [ 'prefix' => '\'', 'suffix' => '|4', 'name' => 'Byonics TinyTrack4' ], [ 'prefix' => '`', 'suffix' => '(5', 'name' => 'Anytone D578UV' ], [ 'prefix' => '`', 'suffix' => '(8', 'name' => 'Anytone D878UV' ] ] ]; /** * Parse MIC-E Packet Data from Raw * * @return boolean (true if successful, false if no MIC-E data present MICE) */ public function getMicEData() { $this->MicEData = []; // Break the path from the body $parts = explode(':', $this->raw ?? '', 2); if (sizeof($parts) == 2) { $mice_raw_path = $parts[0]; $body = $parts[1]; // Mic-E? $micEPrefix = $body[0]; if (($micEPrefix == '`' || $micEPrefix == '\'') && strlen($body) >= 9) { $symbol = $body[8] . $body[7]; // Parse the MIC-E data... This is very order specific as the comment is updated with each pass $pathParts = explode(',', $this->rawPath, 2); if (sizeof($pathParts) > 1) { // Parse MicE Desitnation Address if ($this->_getMicEDataFromToCall()) { $this->_getLongitudeFromMicE($body); $this->_getCommentFromMicE($body); $this->_getRadioTypeFromMicE(); $this->_getAltitudeFromMicE(); return $this->MicEData; } } } } return false; } /** * Parse comment from MIC-E message body * * @param {string} $body (Message Body) * @return boolean * */ private function _getCommentFromMicE($body) { $this->MicEData['comment'] = substr($body, 9); } /** * Parse longitude from Mic-E * @param {string} $body (Message Body) * @return {float} */ private function _getLongitudeFromMicE($body) { $longitudeString = substr($body, 1, 3); //degrees $d = $this->MicEData['longitudeOffset'] + (ord($longitudeString[0]) - 28); if ($d >= 180 && $d <= 189) $d = $d - 80; else if ($d >= 190 && $d <= 199) $d = $d - 190; //minutes $m = ord($longitudeString[1]) - 28; if ($m >= 60) $m = $m - 60; //hundredths of minutes $h = ord($longitudeString[2]) - 28; $minutes = $m + ($h / 100); $this->MicEData['longitude'] = ($this->MicEData['isWest'] ? -1 : 1) * ($d + ($minutes / 60)); return $this->MicEData['comment'] = substr($body, 9); } /** * Reads Data from a Mic-E Destination Address * * @return boolean * */ private function _getMicEDataFromToCall() { $pathParts = explode(',', $this->rawPath, 2); if (sizeof($pathParts) <= 1 || (sizeof($pathParts) > 1 && strlen($pathParts[0]) != 6)) return false; $this->toCall = $pathParts[0]; $latitudeDigits = []; $message = []; $isNorth = false; $longOffset = 0; $isWest = false; for ($i = 1; $i <= 6; $i++) { $values = getInfoForDestinationAddressChar($this->toCall[$i - 1]); array_push($latitudeDigits, $values['latDigit']); if ($i >= 1 && $i <= 3) { array_push($message, $values['message']); } if ($i == 4) $isNorth = $values['isNorth']; if ($i == 5) $longOffset = $values['longOffset']; if ($i == 6) $isWest = $values['isWest']; } $degrees = 10 * $latitudeDigits[0] + $latitudeDigits[1]; $minutes = 10 * $latitudeDigits[2] + $latitudeDigits[3] + ((10 * $latitudeDigits[4] + $latitudeDigits[5]) / 100); $latitude = ($isNorth ? 1 : -1) * $degrees + ($minutes / 60); $this->MicEData = [ 'latitude' => $latitude, 'status' => getStateFromMicE($message), 'longitudeOffset' => $longOffset, 'isWest' => $isWest ]; return true; } /** * Parse altitude from Mic-E * @return {string} */ private function _getAltitudeFromMicE() { if (empty($this->MicEData['comment'])) return; $altitude = null; // Alt is in the format XXX} where XXX = base91 encoded altitude in meters if (preg_match("/(...)}/", substr($this->MicEData['comment'], 0, 5), $msgString) && sizeof($msgString) && $msgString[1]) { $value = 0; for ($i = 0; $i < strlen($msgString[1]); $i++) { $value = $value * 91 + ord($msgString[1][$i]) - 33; } $this->MicEData['altitude'] = $value - 10000; } if (isset($this->MicEData['altitude']) && $this->MicEData['altitude'] != null) { if ($this->altitude == null) $this->altitude = $this->MicEData['altitude']; // Use Mic-E if no other altitude info is available $this->MicEData['comment'] = substr($this->MicEData['comment'], strpos($this->MicEData['comment'], '}') + 1); //Remove altitude info from comment } return $this->MicEData['comment']; } /** * Parse raw comment and set radio type and return packet message as a string * @return {string} */ private function _getRadioTypeFromMicE() { $this->MicEData['equipment'] = null; if (array_search(substr($this->MicEData['comment'], 0, 1), self::radioTypes['knownPrefixes']) !== false) { foreach(self::radioTypes['radios'] as $radio) { if (substr($this->MicEData['comment'], 0, 1) == $radio['prefix'] && substr($this->MicEData['comment'], 0 - strlen($radio['suffix'])) == $radio['suffix']) { $this->MicEData['equipment'] = $radio['name']; //Set radio name $this->MicEData['comment'] = substr($this->MicEData['comment'], strlen($radio['prefix']), strlen($this->MicEData['comment']) - strlen($radio['suffix']) - 1); //Remove radio designators from comment } } } return $this->MicEData['equipment']; } /** * Returns wind direction in cardinal format (i.e. N, S, E, W) * * @return string */ public function getCardinalCourse() { return $this->course != '' ? convertDegreesToCardinalDirection($this->course) : ''; } /** * Returns the capabilities data for a station capability packet * i.e. VK4ZZ-4>APU25N,TCPIP*,qAC,T2PERTH:packetTypeId != 14 || is_null($this->raw) || empty($this->raw)) return null; // Parse the capability data $parts = explode('<', $this->raw, 2); $capabilities = explode(',', $parts[1]); $type = array_shift($capabilities); $capabilityData = []; foreach($capabilities AS $entry) { $data = explode('=', $entry); $capabilityData[$data[0]] = $data[1]; } return [ 'type' => $type, 'data' => $capabilityData ]; } }