Simplified SatPass.kt, predictor now returns this class

This commit is contained in:
Arty Bishop committed 2021-03-23 10:03:42 +00:00
1 parent 33d25d3c25
commit a882a73b8e
15 files changed
+134 -157

No files matched your search

@@ -17,12 +17,31 @@
*/
package com.rtbishop.look4sat.data.model
import com.github.amsacode.predict4java.Satellite
import com.rtbishop.look4sat.utility.PassPredictor
import java.util.*
data class SatPass(
val catNum: Int,
val name: String,
val isDeepSpace: Boolean,
private val aosTime: Long,
val aosAzimuth: Double,
private val losTime: Long,
val losAzimuth: Double,
private val tcaTime: Long,
val tcaAzimuth: Double,
val altitude: Double,
val maxElevation: Double,
val predictor: PassPredictor
) {
val startDate: Date
get() = Date(aosTime)
val endDate: Date
get() = Date(losTime)
val tcaDate: Date
get() = Date(tcaTime)
class SatPass(
val satellite: Satellite,
val predictor: PassPredictor,
val pass: SatPassTime,
var progress: Int = 0
)
}
@@ -1,37 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.data.model
import java.util.*
class SatPassTime(
private val startTime: Date,
private val endTime: Date,
val aosAzimuth: Int,
val losAzimuth: Int,
val maxEl: Double
) {
fun getStartTime(): Date {
return Date(startTime.time)
}
fun getEndTime(): Date {
return Date(endTime.time)
}
}
@@ -62,8 +62,7 @@ class PassesRepo @Inject constructor(
private fun getPasses(satellite: Satellite, refDate: Date): MutableList<SatPass> {
val predictor = satellite.getPredictor(prefsRepo.getStationPosition())
val passes = predictor.getPasses(refDate, prefsRepo.getHoursAhead(), true)
val passList = passes.map { SatPass(satellite, predictor, it) }
return passList as MutableList<SatPass>
return passes as MutableList<SatPass>
}
private fun filterPasses(passes: MutableList<SatPass>, refDate: Date): MutableList<SatPass> {
@@ -72,10 +71,10 @@ class PassesRepo @Inject constructor(
this.time = refDate
this.add(Calendar.HOUR, hoursAhead)
}.time
passes.removeAll { it.pass.getStartTime().after(dateFuture) }
passes.removeAll { it.pass.getEndTime().before(refDate) }
passes.removeAll { it.pass.maxEl < prefsRepo.getMinElevation() }
passes.sortBy { it.pass.getStartTime() }
passes.removeAll { it.startDate.after(dateFuture) }
passes.removeAll { it.endDate.before(refDate) }
passes.removeAll { it.maxElevation < prefsRepo.getMinElevation() }
passes.sortBy { it.startDate }
return passes
}
}
@@ -46,7 +46,8 @@ class EntriesViewModel @Inject constructor(
private val satDataState = MutableSharedFlow<Result<List<SatItem>>>(replay = 0)
private val satDataFlow = satelliteRepo.getAllSatItems().map { Result.Success(it) }
val satData = flowOf(satDataState, satDataFlow).flattenMerge().asLiveData()
val satData =
flowOf(satDataState, satDataFlow).flattenMerge().asLiveData(viewModelScope.coroutineContext)
fun importSatDataFromFile(uri: Uri) {
viewModelScope.launch {
@@ -44,11 +44,11 @@ class PassesAdapter(private val shouldUseUTC: Boolean = false) :
val iterator = satPassList.listIterator()
while (iterator.hasNext()) {
val satPass = iterator.next()
if (!satPass.satellite.tle.isDeepspace) {
if (!satPass.isDeepSpace) {
if (satPass.progress < 100) {
val timeStart = satPass.pass.getStartTime().time
val timeStart = satPass.startDate.time
if (timeNow > timeStart) {
val timeEnd = satPass.pass.getEndTime().time
val timeEnd = satPass.endDate.time
val index = satPassList.indexOf(satPass)
val deltaNow = timeNow.minus(timeStart).toFloat()
val deltaTotal = timeEnd.minus(timeStart).toFloat()
@@ -69,7 +69,7 @@ class PassesAdapter(private val shouldUseUTC: Boolean = false) :
}
override fun getItemViewType(position: Int): Int {
return if (satPassList[position].satellite.tle.isDeepspace) 1
return if (satPassList[position].isDeepSpace) 1
else 0
}
@@ -104,20 +104,19 @@ class PassesAdapter(private val shouldUseUTC: Boolean = false) :
private val endFormat = SimpleDateFormat(endTimeFormat, Locale.getDefault())
fun bind(satPass: SatPass, position: Int, shouldUseUTC: Boolean) {
val satPos = satPass.predictor.getSatPos(Date())
binding.apply {
if (shouldUseUTC) {
startFormat.timeZone = timeZoneUTC
endFormat.timeZone = timeZoneUTC
}
passLeoSatName.text = satPass.satellite.tle.name
passLeoSatId.text = String.format(satIdFormat, satPass.satellite.tle.catnum)
passLeoAosAz.text = String.format(aosAzFormat, satPass.pass.aosAzimuth)
passLeoMaxEl.text = String.format(maxElFormat, satPass.pass.maxEl)
passLeoLosAz.text = String.format(losAzFormat, satPass.pass.losAzimuth)
passLeoStart.text = startFormat.format(satPass.pass.getStartTime())
passLeoAlt.text = String.format(altFormat, satPos.altitude)
passLeoEnd.text = endFormat.format(satPass.pass.getEndTime())
passLeoSatName.text = satPass.name
passLeoSatId.text = String.format(satIdFormat, satPass.catNum)
passLeoAosAz.text = String.format(aosAzFormat, satPass.aosAzimuth)
passLeoMaxEl.text = String.format(maxElFormat, satPass.maxElevation)
passLeoLosAz.text = String.format(losAzFormat, satPass.losAzimuth)
passLeoStart.text = startFormat.format(satPass.startDate)
passLeoAlt.text = String.format(altFormat, satPass.altitude)
passLeoEnd.text = endFormat.format(satPass.endDate)
passLeoProgress.progress = satPass.progress
}
@@ -146,13 +145,12 @@ class PassesAdapter(private val shouldUseUTC: Boolean = false) :
private val elevFormat = itemView.context.getString(R.string.pat_elevation)
fun bind(satPass: SatPass, position: Int) {
val satPos = satPass.predictor.getSatPos(Date())
binding.apply {
passGeoSatName.text = satPass.satellite.tle.name
passGeoSatId.text = String.format(satIdFormat, satPass.satellite.tle.catnum)
passGeoAz.text = String.format(azFormat, Math.toDegrees(satPos.azimuth))
passGeoAlt.text = String.format(altFormat, satPos.altitude)
passGeoEl.text = String.format(elevFormat, Math.toDegrees(satPos.elevation))
passGeoSatName.text = satPass.name
passGeoSatId.text = String.format(satIdFormat, satPass.catNum)
passGeoAz.text = String.format(azFormat, satPass.tcaAzimuth)
passGeoAlt.text = String.format(altFormat, satPass.altitude)
passGeoEl.text = String.format(elevFormat, satPass.maxElevation)
}
itemView.setOnClickListener {
@@ -100,12 +100,12 @@ class PassesFragment : Fragment(R.layout.fragment_passes) {
private fun tickMainTimer(timeNow: Long) {
if (passes.isNotEmpty()) {
try {
val nextPass = passes.first { it.pass.getStartTime().time.minus(timeNow) > 0 }
val millisBeforeStart = nextPass.pass.getStartTime().time.minus(timeNow)
val nextPass = passes.first { it.startDate.time.minus(timeNow) > 0 }
val millisBeforeStart = nextPass.startDate.time.minus(timeNow)
binding?.passesTimer?.text = millisBeforeStart.formatForTimer()
} catch (e: NoSuchElementException) {
val lastPass = passes.last()
val millisBeforeEnd = lastPass.pass.getEndTime().time.minus(timeNow)
val millisBeforeEnd = lastPass.endDate.time.minus(timeNow)
binding?.passesTimer?.text = millisBeforeEnd.formatForTimer()
}
} else {
@@ -26,7 +26,6 @@ import com.rtbishop.look4sat.data.repository.PrefsRepo
import com.rtbishop.look4sat.data.repository.SatelliteRepo
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -37,13 +36,11 @@ class PassesViewModel @Inject constructor(
private val passesRepo: PassesRepo
) : ViewModel() {
val passes = passesRepo.passes.asLiveData()
val passes = passesRepo.passes.asLiveData(viewModelScope.coroutineContext)
init {
viewModelScope.launch {
satelliteRepo.getSelectedSatellitesFlow().collect { selectedSatellites ->
passesRepo.calculatePasses(selectedSatellites)
}
passesRepo.calculatePasses(satelliteRepo.getSelectedSatellites())
}
}
@@ -99,19 +99,10 @@ class PolarFragment : Fragment(R.layout.fragment_polar), SensorEventListener {
binding.frame.addView(polarView)
observeTransmitters()
})
// viewModel.getPasses().observe(viewLifecycleOwner, { result ->
// if (result is Result.Success) {
// satPass = result.data[requireArguments().getInt("index")]
// polarView = PolarView(requireContext()).apply { setPass(satPass) }
// binding.frame.addView(polarView)
// observeTransmitters()
// }
// })
}
private fun observeTransmitters() {
viewModel.getTransmittersForSat(satPass.satellite.tle.catnum)
viewModel.getSatTransmitters(satPass.catNum)
.observe(viewLifecycleOwner, { list ->
transmitterAdapter = TransAdapter(satPass)
if (list.isNotEmpty()) {
@@ -154,14 +145,14 @@ class PolarFragment : Fragment(R.layout.fragment_polar), SensorEventListener {
binding.distance.text = String.format(polarRng, satPos.range)
binding.altitude.text = String.format(polarAlt, satPos.altitude)
if (!satPass.satellite.tle.isDeepspace) {
if (dateNow.before(satPass.pass.getStartTime())) {
val millisBeforeStart = satPass.pass.getStartTime().time.minus(timeNow)
if (!satPass.isDeepSpace) {
if (dateNow.before(satPass.startDate)) {
val millisBeforeStart = satPass.startDate.time.minus(timeNow)
binding.polarTimer.text = millisBeforeStart.formatForTimer()
} else {
val millisBeforeEnd = satPass.pass.getEndTime().time.minus(timeNow)
val millisBeforeEnd = satPass.endDate.time.minus(timeNow)
binding.polarTimer.text = millisBeforeEnd.formatForTimer()
if (dateNow.after(satPass.pass.getEndTime())) {
if (dateNow.after(satPass.endDate)) {
binding.polarTimer.text = 0L.formatForTimer()
findNavController().popBackStack()
}
@@ -72,7 +72,7 @@ class PolarView(context: Context) : View(context) {
canvas.translate(polarCenter, polarCenter)
drawRadarView(canvas)
drawRadarText(canvas)
if (!satPass.satellite.tle.isDeepspace) drawPassTrajectory(canvas, satPass)
if (!satPass.isDeepSpace) drawPassTrajectory(canvas, satPass)
drawSatellite(canvas, satPass)
}
@@ -92,13 +92,13 @@ class PolarView(context: Context) : View(context) {
}
private fun drawPassTrajectory(cvs: Canvas, satPass: SatPass) {
val startTime = satPass.pass.getStartTime()
val endTime = satPass.pass.getEndTime()
val startTime = satPass.startDate
val endTime = satPass.endDate
while (startTime.before(endTime)) {
val satPos = satPass.predictor.getSatPos(startTime)
val x = sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble())
val y = sph2CartY(satPos.azimuth, satPos.elevation, radius.toDouble())
if (startTime.compareTo(satPass.pass.getStartTime()) == 0) {
if (startTime.compareTo(satPass.startDate) == 0) {
path.moveTo(x, -y)
} else {
path.lineTo(x, -y)
@@ -20,8 +20,8 @@ package com.rtbishop.look4sat.ui.polarScreen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.data.model.Result
import com.rtbishop.look4sat.data.model.SatPass
import com.rtbishop.look4sat.data.repository.PassesRepo
import com.rtbishop.look4sat.data.repository.PrefsRepo
import com.rtbishop.look4sat.data.repository.SatelliteRepo
@@ -52,8 +52,6 @@ class PolarViewModel @Inject constructor(
}
}
fun getPasses() = passesRepo.passes.asLiveData()
fun shouldUseCompass(): Boolean {
return prefsRepo.shouldUseCompass()
}
@@ -62,5 +60,6 @@ class PolarViewModel @Inject constructor(
return prefsRepo.getMagDeclination()
}
fun getTransmittersForSat(satId: Int) = satelliteRepo.getSatTransmitters(satId).asLiveData()
fun getSatTransmitters(satId: Int) =
satelliteRepo.getSatTransmitters(satId).asLiveData(viewModelScope.coroutineContext)
}
@@ -25,7 +25,6 @@ import com.rtbishop.look4sat.data.model.SatPass
import com.rtbishop.look4sat.data.model.SatTrans
import com.rtbishop.look4sat.databinding.ItemTransBinding
import com.rtbishop.look4sat.utility.PassPredictor
import com.rtbishop.look4sat.utility.getDopplerFreq
import java.util.*
class TransAdapter(private val pass: SatPass) : RecyclerView.Adapter<TransAdapter.TransHolder>() {
@@ -38,13 +37,8 @@ class TransAdapter(private val pass: SatPass) : RecyclerView.Adapter<TransAdapte
}
fun tickTransmitters() {
if (!pass.satellite.tle.isDeepspace) {
val iterator = transmittersList.listIterator()
while (iterator.hasNext()) {
val trans = iterator.next()
val index = transmittersList.indexOf(trans)
notifyItemChanged(index)
}
if (!pass.isDeepSpace) {
notifyDataSetChanged()
}
}
@@ -63,6 +57,7 @@ class TransAdapter(private val pass: SatPass) : RecyclerView.Adapter<TransAdapte
class TransHolder private constructor(private val binding: ItemTransBinding) :
RecyclerView.ViewHolder(binding.root) {
private val dateNow = Date()
private val divider = 1000000f
private val strNo = itemView.context.getString(R.string.btn_no)
private val strYes = itemView.context.getString(R.string.btn_yes)
@@ -74,7 +69,7 @@ class TransAdapter(private val pass: SatPass) : RecyclerView.Adapter<TransAdapte
fun bind(satTrans: SatTrans, satPass: SatPass) {
binding.description.text = satTrans.description
if (satPass.satellite.tle.isDeepspace) setRegularFreq(satTrans)
if (satPass.isDeepSpace) setRegularFreq(satTrans)
else setDopplerFreq(satTrans, satPass.predictor)
if (satTrans.mode != null) binding.mode.text = String.format(mode, satTrans.mode)
@@ -97,18 +92,14 @@ class TransAdapter(private val pass: SatPass) : RecyclerView.Adapter<TransAdapte
}
private fun setDopplerFreq(satTrans: SatTrans, predictor: PassPredictor) {
val dateNow = Date()
if (satTrans.downlinkLow != null) {
val rangeRate = predictor.getSatPos(dateNow).rangeRate
val freq = satTrans.downlinkLow.getDopplerFreq(rangeRate, true) / divider
binding.downlink.text = String.format(Locale.ENGLISH, formatLink, freq)
val downlink = predictor.getDownlinkFreq(satTrans.downlinkLow, dateNow) / divider
binding.downlink.text = String.format(Locale.ENGLISH, formatLink, downlink)
} else binding.downlink.text = formatLinkNull
if (satTrans.uplinkLow != null) {
val rangeRate = predictor.getSatPos(dateNow).rangeRate
val freq = satTrans.uplinkLow.getDopplerFreq(rangeRate, false) / divider
binding.uplink.text = String.format(Locale.ENGLISH, formatLink, freq)
val uplink = predictor.getUplinkFreq(satTrans.uplinkLow, dateNow) / divider
binding.uplink.text = String.format(Locale.ENGLISH, formatLink, uplink)
} else binding.uplink.text = formatLinkNull
}
@@ -51,15 +51,6 @@ fun Long.formatForTimer(): String {
return String.format(format, hours, minutes, seconds)
}
fun Long.getDopplerFreq(rangeRate: Double, isDownlink: Boolean): Long {
val speedOfLight = 2.99792458E8
return if (isDownlink) {
(this.toDouble() * (speedOfLight - rangeRate * 1000.0) / speedOfLight).toLong()
} else {
(this.toDouble() * (speedOfLight + rangeRate * 1000.0) / speedOfLight).toLong()
}
}
fun Double.round(decimals: Int): Double {
var multiplier = 1.0
repeat(decimals) { multiplier *= 10 }
@@ -20,7 +20,7 @@ package com.rtbishop.look4sat.utility
import com.github.amsacode.predict4java.GroundStationPosition
import com.github.amsacode.predict4java.SatPos
import com.github.amsacode.predict4java.Satellite
import com.rtbishop.look4sat.data.model.SatPassTime
import com.rtbishop.look4sat.data.model.SatPass
import java.util.*
class PassPredictor(private val satellite: Satellite, private val qth: GroundStationPosition) {
@@ -53,9 +53,9 @@ class PassPredictor(private val satellite: Satellite, private val qth: GroundSta
}
return positions
}
fun getPasses(refDate: Date, hoursAhead: Int, windBack: Boolean): List<SatPassTime> {
val passes = mutableListOf<SatPassTime>()
fun getPasses(refDate: Date, hoursAhead: Int, windBack: Boolean): List<SatPass> {
val passes = mutableListOf<SatPass>()
val endDate = Date(refDate.time + hoursAhead * 60L * 60L * 1000L)
var startDate = refDate
var shouldWindBack = windBack
@@ -68,34 +68,45 @@ class PassPredictor(private val satellite: Satellite, private val qth: GroundSta
do {
if (count > 0) shouldWindBack = false
val pass = nextNearEarthPass(startDate, shouldWindBack)
lastAosDate = pass.getStartTime()
lastAosDate = pass.startDate
passes.add(pass)
startDate =
Date(pass.getEndTime().time + (oneQuarterOrbitMin * 3) * 60L * 1000L)
Date(pass.endDate.time + (oneQuarterOrbitMin * 3) * 60L * 1000L)
count++
} while (lastAosDate < endDate)
}
}
return passes
}
private fun nextDeepSpacePass(refDate: Date): SatPassTime {
private fun nextDeepSpacePass(refDate: Date): SatPass {
val satPos = getSatPos(refDate)
val startDate = Date(refDate.time - 24 * 60L * 60L * 1000L)
val endDate = Date(refDate.time + 24 * 60L * 60L * 1000L)
val azimuth = Math.toDegrees(satPos.azimuth).toInt()
val maxEl = Math.toDegrees(satPos.elevation)
return SatPassTime(startDate, endDate, azimuth, azimuth, maxEl)
val id = satellite.tle.catnum
val name = satellite.tle.name
val isDeep = satellite.tle.isDeepspace
val aos = Date(refDate.time - 24 * 60L * 60L * 1000L).time
val los = Date(refDate.time + 24 * 60L * 60L * 1000L).time
val tca = Date((aos + los) / 2).time
val az = Math.toDegrees(satPos.azimuth)
val elev = Math.toDegrees(satPos.elevation)
val alt = satPos.altitude
return SatPass(id, name, isDeep, aos, az, los, az, tca, az, alt, elev, this)
}
private fun nextNearEarthPass(refDate: Date, windBack: Boolean = false): SatPassTime {
private fun nextNearEarthPass(refDate: Date, windBack: Boolean = false): SatPass {
val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
clear()
timeInMillis = refDate.time
}
var maxElevation = 0.0
val id = satellite.tle.catnum
val name = satellite.tle.name
val isDeep = satellite.tle.isDeepspace
var elevation: Double
var maxElevation = 0.0
var alt = 0.0
var tcaAz = 0.0
// wind back time 1/4 of an orbit
if (windBack) calendar.add(Calendar.MINUTE, -oneQuarterOrbitMin)
var satPos = getSatPos(calendar.time)
@@ -115,7 +126,11 @@ class PassPredictor(private val satellite: Satellite, private val qth: GroundSta
calendar.add(Calendar.SECOND, 60)
satPos = getSatPos(calendar.time)
elevation = satPos.elevation
if (elevation > maxElevation) maxElevation = elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation < 0.0)
// refine to 3 seconds
@@ -124,18 +139,26 @@ class PassPredictor(private val satellite: Satellite, private val qth: GroundSta
calendar.add(Calendar.SECOND, 3)
satPos = getSatPos(calendar.time)
elevation = satPos.elevation
if (elevation > maxElevation) maxElevation = elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation < 0.0)
val startDate = satPos.time
val aosAzimuth = Math.toDegrees(satPos.azimuth).toInt()
val aos = satPos.time.time
val aosAz = Math.toDegrees(satPos.azimuth)
// find when sat goes below
do {
calendar.add(Calendar.SECOND, 30)
satPos = getSatPos(calendar.time)
elevation = satPos.elevation
if (elevation > maxElevation) maxElevation = elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation > 0.0)
// refine to 3 seconds
@@ -144,12 +167,17 @@ class PassPredictor(private val satellite: Satellite, private val qth: GroundSta
calendar.add(Calendar.SECOND, 3)
satPos = getSatPos(calendar.time)
elevation = satPos.elevation
if (elevation > maxElevation) maxElevation = elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation > 0.0)
val endDate = satPos.time
val losAzimuth = Math.toDegrees(satPos.azimuth).toInt()
val maxEl = Math.toDegrees(maxElevation)
return SatPassTime(startDate, endDate, aosAzimuth, losAzimuth, maxEl)
val los = satPos.time.time
val losAz = Math.toDegrees(satPos.azimuth)
val tca = Date((aos + los) / 2).time
val elev = Math.toDegrees(maxElevation)
return SatPass(id, name, isDeep, aos, aosAz, los, losAz, tca, tcaAz, alt, elev, this)
}
}
+2 -2
View File
@@ -22,9 +22,9 @@
<string name="passes_error">Нет предстоящих пролётов</string>
<string name="pass_aosAz">НПС - %2d°</string>
<string name="pass_aosAz">НПС - %.1f°</string>
<string name="pass_maxEl">МаксЭлев: %.1f°</string>
<string name="pass_losAz">%2d° - КПС</string>
<string name="pass_losAz">%.1f° - КПС</string>
<string name="trans_mode">Модуляция: %s</string>
<string name="trans_no_mode">Модуляция: нет</string>
+2 -2
View File
@@ -30,9 +30,9 @@
<string name="pass_satName" translatable="false">%s</string>
<string name="pass_satId" translatable="false">Id:%d</string>
<string name="pass_aosAz">AOS - %2d°</string>
<string name="pass_aosAz">AOS - %.1f°</string>
<string name="pass_maxEl">MaxEl: %.1f°</string>
<string name="pass_losAz">%2d° - LOS</string>
<string name="pass_losAz">%.1f° - LOS</string>
<string name="pass_startTime" translatable="false">HH:mm:ss - EEE</string>
<string name="pass_endTime" translatable="false">EEE - HH:mm:ss</string>