Prettify main view, add temperature history
This commit is contained in:
@ -55,7 +55,9 @@ final class BluetoothClient: ObservableObject {
|
||||
private var runningTransfer: TemperatureDataTransfer?
|
||||
|
||||
func updateDeviceInfo() {
|
||||
addRequest(.getInfo)
|
||||
if case .configured = deviceState {
|
||||
addRequest(.getInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private var dataUpdateTimer: Timer?
|
||||
|
@ -3,20 +3,56 @@ import SFSafeSymbols
|
||||
import BottomSheet
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
|
||||
private let updateInterval = 1.0
|
||||
|
||||
private let minTempColor = Color(hue: 0.624, saturation: 0.5, brightness: 1.0)
|
||||
private let minTemperature = -20.0
|
||||
|
||||
private let maxTempColor = Color(hue: 1.0, saturation: 0.5, brightness: 1.0)
|
||||
private let maxTemperature = 40.0
|
||||
|
||||
private let disconnectedColor = Color(white: 0.8)
|
||||
|
||||
@ObservedObject
|
||||
var client = BluetoothClient()
|
||||
|
||||
init() {
|
||||
|
||||
}
|
||||
|
||||
init(client: BluetoothClient) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
|
||||
@ObservedObject
|
||||
var storage = TemperatureStorage()
|
||||
|
||||
@State
|
||||
var showDeviceInfo = false
|
||||
|
||||
@State
|
||||
var updateTimer: Timer?
|
||||
|
||||
@State
|
||||
var updateInfoToggle = true
|
||||
|
||||
init() {
|
||||
startRegularUpdates()
|
||||
}
|
||||
|
||||
init(client: BluetoothClient, values: [TemperatureMeasurement]) {
|
||||
self.client = client
|
||||
self.storage = .init(lastMeasurements: values)
|
||||
startRegularUpdates()
|
||||
}
|
||||
|
||||
private func startRegularUpdates() {
|
||||
guard updateTimer == nil else {
|
||||
return
|
||||
}
|
||||
updateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { timer in
|
||||
self.updateInfoToggle.toggle()
|
||||
}
|
||||
|
||||
updateTimer?.fire()
|
||||
}
|
||||
|
||||
var hasDeviceInfo: Bool {
|
||||
client.deviceInfo != nil
|
||||
}
|
||||
|
||||
var averageTemperature: Double? {
|
||||
let t1 = client.deviceInfo?.sensor1?.optionalValue
|
||||
@ -54,54 +90,77 @@ struct ContentView: View {
|
||||
}
|
||||
return .thermometerHigh
|
||||
}
|
||||
|
||||
|
||||
var backgroundColor: Color {
|
||||
guard let temp = averageTemperature else {
|
||||
return disconnectedColor
|
||||
}
|
||||
guard temp > minTemperature else {
|
||||
return minTempColor
|
||||
}
|
||||
guard temp < maxTemperature else {
|
||||
return maxTempColor
|
||||
}
|
||||
let ratio = (temp - minTemperature) / (maxTemperature - minTemperature)
|
||||
return minTempColor.blend(to: maxTempColor, intensity: ratio)
|
||||
}
|
||||
|
||||
var backgroundGradient: Gradient {
|
||||
let color = backgroundColor
|
||||
let lighter = color.opacity(0.5)
|
||||
return .init(colors: [lighter, color])
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemSymbol: .iphone)
|
||||
.frame(width: 30)
|
||||
Text(client.deviceState.text)
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
Image(systemSymbol: temperatureIcon)
|
||||
.font(.system(size: 200, weight: .light))
|
||||
// Image(systemSymbol: temperatureIcon)
|
||||
// .font(.system(size: 100, weight: .light))
|
||||
if hasTemperature {
|
||||
Text(temperatureString)
|
||||
.font(.system(size: 100, weight: .light))
|
||||
.font(.system(size: 150, weight: .light))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
TemperatureHistoryChart(points: storage.lastMeasurements)
|
||||
.frame(height: 150)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
HStack(alignment: .center) {
|
||||
Button(action: {
|
||||
_ = client.collectRecordedData()
|
||||
}) {
|
||||
Text("Transfer")
|
||||
}.padding()
|
||||
Spacer()
|
||||
Button {
|
||||
self.showDeviceInfo = true
|
||||
} label: {
|
||||
Image(systemSymbol: .infoCircle)
|
||||
.font(.system(size: 40, weight: .regular))
|
||||
}.disabled(client.deviceInfo == nil)
|
||||
if hasDeviceInfo {
|
||||
Image(systemSymbol: .iphone)
|
||||
.font(.system(size: 30, weight: .regular))
|
||||
}
|
||||
Text(client.deviceState.text)
|
||||
}
|
||||
.disabled(!hasDeviceInfo)
|
||||
.foregroundColor(.white)
|
||||
|
||||
}.padding()
|
||||
}
|
||||
.padding()
|
||||
.bottomSheet(isPresented: $showDeviceInfo, height: 520) {
|
||||
.bottomSheet(isPresented: $showDeviceInfo, height: 600) {
|
||||
if let info = client.deviceInfo {
|
||||
DeviceInfoView(info: info)
|
||||
DeviceInfoView(
|
||||
info: info,
|
||||
isPresented: $showDeviceInfo, updateToggle: $updateInfoToggle)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
.background(backgroundGradient)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView(client: BluetoothClient(deviceInfo: .mock))
|
||||
ContentView(
|
||||
client: BluetoothClient(deviceInfo: .mock),
|
||||
values: TemperatureMeasurement.mockData)
|
||||
}
|
||||
}
|
||||
|
||||
|
24
TempTrack/Extensions/Color+Extensions.swift
Normal file
24
TempTrack/Extensions/Color+Extensions.swift
Normal file
@ -0,0 +1,24 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
|
||||
func blend(to other: Color, intensity: CGFloat = 0.5) -> Color {
|
||||
Color(UIColor(self).blend(to: UIColor(other), intensity: intensity))
|
||||
}
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
|
||||
func blend(to other: UIColor, intensity: CGFloat = 0.5) -> UIColor {
|
||||
let l2 = max(0.0, min(1.0, intensity))
|
||||
let l1 = 1 - l2
|
||||
var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
|
||||
var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
|
||||
|
||||
getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
|
||||
other.getRed(&r2, green: &g2, blue: &b2, alpha: &a2)
|
||||
|
||||
return UIColor(red: l1*r1 + l2*r2, green: l1*g1 + l2*g2, blue: l1*b1 + l2*b2, alpha: l1*a1 + l2*a2)
|
||||
}
|
||||
}
|
@ -1,10 +1,3 @@
|
||||
//
|
||||
// TempTrackApp.swift
|
||||
// TempTrack
|
||||
//
|
||||
// Created by iMac on 29.05.23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
|
@ -1,10 +1,113 @@
|
||||
import Foundation
|
||||
|
||||
struct TemperatureMeasurement {
|
||||
struct TemperatureMeasurement: Identifiable {
|
||||
|
||||
var sensor0: TemperatureValue
|
||||
|
||||
var sensor1: TemperatureValue
|
||||
|
||||
var date: Date
|
||||
|
||||
var id: Int {
|
||||
Int(date.timeIntervalSince1970.rounded())
|
||||
}
|
||||
|
||||
var secondsAgo: Int {
|
||||
Int(date.timeIntervalSinceNow.rounded())
|
||||
}
|
||||
}
|
||||
|
||||
private extension TemperatureValue {
|
||||
|
||||
init(value: Double?) {
|
||||
if let value {
|
||||
self = .value(value)
|
||||
} else {
|
||||
self = .notFound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension TemperatureMeasurement {
|
||||
|
||||
init(t0: Double?, t1: Double?, secs: Int) {
|
||||
self.sensor0 = .init(value: t0)
|
||||
self.sensor1 = .init(value: t1)
|
||||
self.date = Date().addingTimeInterval(TimeInterval(secs-3600))
|
||||
}
|
||||
|
||||
init(t0: Double?, t1: Double?, min: Int) {
|
||||
self.init(t0: t0, t1: t1, secs: min * 60)
|
||||
}
|
||||
}
|
||||
|
||||
extension TemperatureMeasurement {
|
||||
|
||||
static let mockData: [TemperatureMeasurement] = {
|
||||
let temps: [(Double?, Double?)] = [
|
||||
(20, 14),
|
||||
(20, 13.5),
|
||||
(20.5, 13.5),
|
||||
(20.5, 13.5),
|
||||
(21, 14),
|
||||
(21, 14),
|
||||
(nil, 14.5),
|
||||
(nil, 14),
|
||||
(nil, 14.5),
|
||||
(nil, 14),
|
||||
(nil, 14),
|
||||
(nil, 14.5),
|
||||
(nil, 15),
|
||||
(5.0, 15),
|
||||
(4.5, 15.5),
|
||||
(4.5, 16),
|
||||
(4.0, 16.5),
|
||||
(3.0, 17),
|
||||
(3.0, 19),
|
||||
(2.5, 20),
|
||||
(2.5, 20.5),
|
||||
(2.0, 20.5),
|
||||
(1.0, 20.5),
|
||||
(0.5, 20.5),
|
||||
(0.0, 20),
|
||||
(0.0, 20),
|
||||
(-1.0, 21.0),
|
||||
(-0.5, 21.0),
|
||||
(-3.0, 21.0),
|
||||
(-3.5, 20.5),
|
||||
(-4.0, 20.5),
|
||||
(-5.0, 20.0),
|
||||
(-5.0, nil),
|
||||
(-5.5, nil),
|
||||
(-5.0, nil),
|
||||
(-5.5, nil),
|
||||
(-6.0, nil),
|
||||
(-5.0, nil),
|
||||
(nil, nil),
|
||||
(nil, nil),
|
||||
(nil, nil),
|
||||
(-5.0, nil),
|
||||
(-4.5, nil),
|
||||
(-4.0, 23.0),
|
||||
(5.0, 24.0),
|
||||
(7.0, 25.0),
|
||||
(8.0, 25.5),
|
||||
(8.5, 25.5),
|
||||
(10.0, 25.5),
|
||||
(10.5, 24.0),
|
||||
(10.5, 24.0),
|
||||
(10.5, 24.5),
|
||||
(12.0, 23.5),
|
||||
(12.5, 24.0),
|
||||
(12.0, 23.5),
|
||||
(14.0, 24.0),
|
||||
(15.0, 25.0),
|
||||
(15.0, 25.0),
|
||||
(15.5, 25.0),
|
||||
(15.0, 25.0),
|
||||
]
|
||||
return temps.enumerated().map {
|
||||
TemperatureMeasurement(t0: $0.element.0, t1: $0.element.1, min: $0.offset)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -36,8 +36,8 @@ enum TemperatureValue {
|
||||
return "No sensor"
|
||||
case .invalidMeasurement:
|
||||
return "Invalid"
|
||||
case .value(let double):
|
||||
return "\(Int(double.rounded()))°C"
|
||||
case .value(let value):
|
||||
return String(format:" %.1f°C", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,70 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import SQLite
|
||||
|
||||
final class TemperatureStorage {
|
||||
final class TemperatureStorage: ObservableObject {
|
||||
|
||||
static var documentDirectory: URL {
|
||||
try! FileManager.default.url(
|
||||
for: .documentDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: nil, create: true)
|
||||
}
|
||||
|
||||
private let databaseUrl: URL
|
||||
|
||||
@Published
|
||||
var lastMeasurements: [TemperatureMeasurement]
|
||||
|
||||
init(lastMeasurements: [TemperatureMeasurement] = []) {
|
||||
self.lastMeasurements = lastMeasurements
|
||||
self.databaseUrl = TemperatureStorage.documentDirectory.appendingPathComponent("db.sqlite3")
|
||||
}
|
||||
|
||||
private let table = Table("values")
|
||||
private let i
|
||||
|
||||
private func createDatabaseIfNeeded() throws {
|
||||
let db = try Connection(databaseUrl.path)
|
||||
|
||||
let users = Table("users")
|
||||
let id = Expression<Int64>("id")
|
||||
let name = Expression<String?>("name")
|
||||
let email = Expression<String>("email")
|
||||
|
||||
try db.run(users.create(ifNotExists: true) { t in
|
||||
t.column(id, primaryKey: true)
|
||||
t.column(name)
|
||||
t.column(email, unique: true)
|
||||
})
|
||||
// CREATE TABLE "users" (
|
||||
// "id" INTEGER PRIMARY KEY NOT NULL,
|
||||
// "name" TEXT,
|
||||
// "email" TEXT NOT NULL UNIQUE
|
||||
// )
|
||||
|
||||
let insert = users.insert(name <- "Alice", email <- "alice@mac.com")
|
||||
let rowid = try db.run(insert)
|
||||
// INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com')
|
||||
|
||||
for user in try db.prepare(users) {
|
||||
print("id: \(user[id]), name: \(user[name]), email: \(user[email])")
|
||||
// id: 1, name: Optional("Alice"), email: alice@mac.com
|
||||
}
|
||||
// SELECT * FROM "users"
|
||||
|
||||
let alice = users.filter(id == rowid)
|
||||
|
||||
try db.run(alice.update(email <- email.replace("mac.com", with: "me.com")))
|
||||
// UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com')
|
||||
// WHERE ("id" = 1)
|
||||
|
||||
try db.run(alice.delete())
|
||||
// DELETE FROM "users" WHERE ("id" = 1)
|
||||
|
||||
try db.scalar(users.count) // 0
|
||||
// SELECT count(*) FROM "users"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,12 @@ struct DeviceInfoView: View {
|
||||
private let storageWarnBytes = 500
|
||||
|
||||
let info: DeviceInfo
|
||||
|
||||
@Binding
|
||||
var isPresented: Bool
|
||||
|
||||
@Binding
|
||||
var updateToggle: Bool
|
||||
|
||||
private var runTimeString: String {
|
||||
let number = info.numberOfSecondsRunning
|
||||
@ -97,6 +103,16 @@ struct DeviceInfoView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text("Device Info").font(.title2).bold()
|
||||
Spacer()
|
||||
Button(action: { isPresented = false }) {
|
||||
Image(systemSymbol: .xmarkCircleFill)
|
||||
.foregroundColor(.gray)
|
||||
.font(.system(size: 26))
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Recording")
|
||||
.font(.headline)
|
||||
@ -159,8 +175,11 @@ struct DeviceInfoView: View {
|
||||
|
||||
struct DeviceInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DeviceInfoView(info: .mock)
|
||||
.previewLayout(.fixed(width: 375, height: 500))
|
||||
DeviceInfoView(
|
||||
info: .mock,
|
||||
isPresented: .constant(true),
|
||||
updateToggle: .constant(true))
|
||||
.previewLayout(.fixed(width: 375, height: 600))
|
||||
}
|
||||
}
|
||||
|
||||
|
58
TempTrack/Views/TemperatureHistoryChart.swift
Normal file
58
TempTrack/Views/TemperatureHistoryChart.swift
Normal file
@ -0,0 +1,58 @@
|
||||
import SwiftUI
|
||||
import Charts
|
||||
|
||||
struct TemperatureHistoryChart: View {
|
||||
|
||||
let points: [TemperatureMeasurement]
|
||||
|
||||
let upperTempLimit = 40.0
|
||||
let lowerTempLimit = -20.0
|
||||
|
||||
let pastDateLimit = -3600
|
||||
let futureDateLimit = 0
|
||||
|
||||
var body: some View {
|
||||
Chart {
|
||||
ForEach(points) { point in
|
||||
if let s = point.sensor0.optionalValue {
|
||||
LineMark(
|
||||
x: .value("Date", point.secondsAgo),
|
||||
y: .value("Temperature", s))
|
||||
.foregroundStyle(Color.red)
|
||||
}
|
||||
if let s = point.sensor1.optionalValue {
|
||||
LineMark(
|
||||
x: .value("Date", point.secondsAgo),
|
||||
y: .value("Temperature", s))
|
||||
.foregroundStyle(by: .value("Type", "Sensor 1"))
|
||||
}
|
||||
}
|
||||
}
|
||||
.chartXScale(domain: pastDateLimit...futureDateLimit)
|
||||
.chartYScale(domain: lowerTempLimit...upperTempLimit)
|
||||
.chartXAxis(.hidden)
|
||||
.chartLegend(.hidden)
|
||||
.chartYAxis {
|
||||
AxisMarks(position: .trailing, values: .automatic) { value in
|
||||
AxisValueLabel(multiLabelAlignment: .trailing) {
|
||||
if let intValue = value.as(Int.self) {
|
||||
Text("\(intValue) km")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
//AxisMarks(position: .trailing, stroke: StrokeStyle(lineWidth: 0))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct TemperatureHistoryChart_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TemperatureHistoryChart(
|
||||
points: TemperatureMeasurement.mockData)
|
||||
.previewLayout(.fixed(width: 350, height: 150))
|
||||
.background(.gray)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user