“我用 400 行 Swift 代码给破旧的自行车加了一个动感单车计步器!”

2023-10-18 22:30

本文主要是介绍“我用 400 行 Swift 代码给破旧的自行车加了一个动感单车计步器!”,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇

用软件改装,让原来破旧的自行车在功能上焕然一新。

以下为译文:

最近我跟朋友聊起SwiftUI。SwiftUI刚发布第一年的时候并不怎么好用,但幸运的是当时我并没有使用。后来,我掌握了这门语言之后,它就成了我所有快乐的源泉。朋友问,“为什么?”我略加思索,然后说:“我喜欢做原型,而SwiftUI扫清了许多我早已习惯的障碍。”

回想起远古时代,我做技术原型时喜欢用Objective-C编写UI。它的优点是你可以在一张图中看到所有逻辑。相应地,副作用就是很难让人集中注意力。

SwiftUI可以带来相同的感觉,不过更为简洁,而且也没有副作用。

以我最近的一个项目为例:我有一台廉价的动感单车,用来锻炼身体很合适,但它的界面非常不友好。我一直想要一个显示屏!用单片机和信号线自己做一个显示屏?然后用计算机视觉来处理数据?

或者,也许可以完全不管显示屏的问题,而是根据手机的传感器来估算动感单车的速度,然后计算其他数据?

可行吗?

我之前在硬件项目里接触过九轴的传感器,了解应该通过怎样的运动来进行测量。尽管理论上我知道应该在动感单车上采用哪种传感器(加速度计和陀螺仪),但我不确定踩踏板的动作能否可靠地被某个传感器识别。而且,即使能识别,这种数据也有很大噪声。我需要一个原型。

iPhone的九轴传感器会输出一个双精度型数组,但与其他电动设备一样,这些采样数据只是真实运动的片面表示而已。所以,在提取采样数据之后,还需要进行平滑处理。如果一切可行,就应该能用可视化的方式来表示数据,比如画出传感器数据的图表。


Swift Charts

在动笔之前,我尝试了SwiftUI的所有图表库,但没有一个能满足我的要求。我想了几天,决定先选一个,以后再慢慢改进,但我突然发现,苹果恰好在WWDC上发布了一个非常好用的图表框架!这个框架正好能满足我原型的需要。但这也意味着,下面的代码只能在Xcode 14 Beta上运行,也只能在iOS 16 beta的设备上运行。

目标

用最少的代码,为每个传感器实现一个图表。不需要考虑状态和错误,只需要展示数据,可以认为设备全部正常工作。也不需要考虑用户交互。功能要求如下:

1.能查看所有传感器。

2.当没有传感器数据时关闭视图。

3.分开显示传感器的三个轴的数据。

4.平滑数据,并计算波峰的数量(等价于踩踏板的次数)。

原型的目的是验证这个思路是否可行,所以一切从简,只需找出问题的答案即可。而实际的产品则会考虑另一个问题:“是否需要通用化?”而至少目前该问题的答案是否定的。

在应用程序中,我不会把模型和界面放在同一个文件中。但是,另一个我喜欢SwiftUI的点是,你只需要写一个文件放到应用中,然后在App的构造中初始化,即可看到UI。太棒了。由于我的目的只是尝试,所以只需要使用一个文件。不过我在一些有意思的地方加了注释。

代码

ContentView.swift

1// Created by Halle Winkler on July/11/22. Copyright © 2022. All rights reserved.2// Requires Xcode 14.x and iOS 16.x, betas included.34import Charts5import CoreMotion6import SwiftUI78// MARK: - ContentView910/// ContentView is a collection of motion sensor UIs and a method of calling back to the model.1112struct ContentView {13    @ObservedObject var manager: MotionManager14}1516extension ContentView: View {17    var body: some View {18        VStack {19            ForEach(manager.sensors, id: \.sensorName) { sensor in20                SensorChart(sensor: sensor) { applyFilter, lowPassFilterFactor, quantizeFactor in21                    manager.updateFilteringFor(22                        sensor: sensor,23                        applyFilter: applyFilter,24                        lowPassFilterFactor: lowPassFilterFactor,25                        quantizeFactor: quantizeFactor)26                }27            }28        }.padding([.leading, .trailing], 6)29    }30}3132// MARK: - SensorChart3334/// I like to compose SwiftUI interfaces out of many small modules. But, there is a tension when it's a35/// small UI overall, and the modules will each have overhead from propagating state, binding and callbacks.3637struct SensorChart {38    @State private var chartIsVisible = true39    @State private var breakOutAxes = false40    @State private var applyingFilter = false41    @State private var lowPassFilterFactor: Double = 0.7542    @State private var quantizeFactor: Double = 5043    var sensor: Sensor44    let updateFiltering: (Bool, Double, Double) -> Void45    private func toggleFiltering() {46        applyingFilter.toggle()47        updateFiltering(applyingFilter, lowPassFilterFactor, quantizeFactor)48    }49}5051extension SensorChart: View {52    var body: some View {53/// Per-sensor controls: apply filtering to the waveform, hide and show sensor, break out the axes into separate charts.5455        HStack {56            Text("\(sensor.sensorName)")57                .font(.system(size: 12, weight: .semibold, design: .default))58                .foregroundColor(chartIsVisible ? .black : .gray)59            Spacer()60            Button(action: toggleFiltering) {61                Image(systemName: applyingFilter ? "waveform.circle.fill" :62                    "waveform.circle")63            }64            .opacity(chartIsVisible ? 1.0 : 0.0)65            Button(action: { chartIsVisible.toggle() }) {66                Image(systemName: chartIsVisible ? "eye.circle.fill" :67                    "eye.slash.circle")68            }69            Button(action: { breakOutAxes.toggle() }) {70                Image(systemName: breakOutAxes ? "1.circle.fill" :71                    "3.circle.fill")72            }73            .opacity(chartIsVisible ? 1.0 : 0.0)74        }7576/// Sensor charts, either one chart with three axes, or three charts with one axis. I love how concise Swift Charts can be.7778        if chartIsVisible {79            if breakOutAxes {80                ForEach(sensor.axes, id: \.axisName) { series in81                    // Iterate charts from series82                    Chart {83                        ForEach(84                            Array(series.measurements.enumerated()),85                            id: \.offset) { index, datum in86                                LineMark(87                                    x: .value("Count", index),88                                    y: .value("Measurement", datum))89                            }90                    }91                    Text(92                        "Axis: \(series.axisName)\(applyingFilter ? "\t\tPeaks in window: \(series.peaks)" : "")")93                }94                .chartXAxis {95                    AxisMarks(values: .automatic(desiredCount: 0))96                }97            } else {98                Chart {99                    ForEach(sensor.axes, id: \.axisName) { series in
100                        // Iterate series in a chart
101                        ForEach(
102                            Array(series.measurements.enumerated()),
103                            id: \.offset) { index, datum in
104                                LineMark(
105                                    x: .value("Count", index),
106                                    y: .value("Measurement", datum))
107                            }
108                            .foregroundStyle(by: .value("MeasurementName",
109                                                        series.axisName))
110                    }
111                }.chartXAxis {
112                    AxisMarks(values: .automatic(desiredCount: 0))
113                }.chartYAxis {
114                    AxisMarks(values: .automatic(desiredCount: 2))
115                }
116            }
117
118/// in the separate three-axis view, you can set the low-pass filter factor and the quantizing factor if the waveform
119/// filtering is on, and then once you can see your stationary pedaling reflected in the waveform, you can see how
120/// many times per time window you're pedaling. With such an inevitably-noisy sensor environment, I already know
121/// the low-pass filter factor will have to be very high, so I'm starting it at 0.75.
122/// In the case of my exercise bike, the quantizing factor  that delivers very accurate peak-counting results on
123/// gyroscope axis z is 520, which tells you these readings are really small numbers.
124
125            if applyingFilter {
126                Slider(
127                    value: $lowPassFilterFactor,
128                    in: 0.75 ... 1.0,
129                    onEditingChanged: { _ in
130                        updateFiltering(
131                            true,
132                            lowPassFilterFactor,
133                            quantizeFactor)
134                    })
135                Text("Lowpass: \(String(format: "%.2f", lowPassFilterFactor))")
136                    .font(.system(size: 12))
137                    .frame(width: 100, alignment: .trailing)
138                Slider(
139                    value: $quantizeFactor,
140                    in: 1 ... 600,
141                    onEditingChanged: { _ in
142                        updateFiltering(
143                            true,
144                            lowPassFilterFactor,
145                            quantizeFactor)
146                    })
147                Text("Quantize: \(Int(quantizeFactor))")
148                    .font(.system(size: 12))
149                    .frame(width: 100, alignment: .trailing)
150            }
151        }
152        Divider()
153    }
154}
155
156// MARK: - MotionManager
157
158/// MotionManager is the sensor management module.
159
160class MotionManager: ObservableObject {
161    // MARK: Lifecycle
162
163    init() {
164        self.manager = CMMotionManager()
165        for name in SensorNames
166            .allCases {
167// self.sensors and func collectReadings(...) use SensorNames to index,
168            if name ==
169                .attitude {
170// so if you change how one creates/derives a sensor index, change them both.
171                sensors.append(ThreeAxisReadings(
172                    sensorName: SensorNames.attitude.rawValue,
173                    // The one exception to sensor axis naming:
174                    axes: [
175                        Axis(axisName: "Pitch"),
176                        Axis(axisName: "Roll"),
177                        Axis(axisName: "Yaw"),
178                    ]))
179            } else {
180                sensors.append(ThreeAxisReadings(sensorName: name.rawValue))
181            }
182        }
183        self.manager.deviceMotionUpdateInterval = sensorUpdateInterval
184        self.manager.accelerometerUpdateInterval = sensorUpdateInterval
185        self.manager.gyroUpdateInterval = sensorUpdateInterval
186        self.manager.magnetometerUpdateInterval = sensorUpdateInterval
187        self.startDeviceUpdates(manager: manager)
188    }
189
190    // MARK: Public
191
192    public func updateFilteringFor( // Manage the callbacks from the UI
193        sensor: ThreeAxisReadings,
194        applyFilter: Bool,
195        lowPassFilterFactor: Double,
196        quantizeFactor: Double) {
197        guard let index = sensors.firstIndex(of: sensor) else { return }
198        DispatchQueue.main.async {
199            self.sensors[index].applyFilter = applyFilter
200            self.sensors[index].lowPassFilterFactor = lowPassFilterFactor
201            self.sensors[index].quantizeFactor = quantizeFactor
202        }
203    }
204
205    // MARK: Internal
206
207    struct ThreeAxisReadings: Equatable {
208        var sensorName: String // Usually, these have the same naming:
209        var axes: [Axis] = [Axis(axisName: "x"), Axis(axisName: "y"),
210                            Axis(axisName: "z")]
211        var applyFilter: Bool = false
212        var lowPassFilterFactor = 0.75
213        var quantizeFactor = 1.0
214
215        func lowPassFilter(lastReading: Double?, newReading: Double) -> Double {
216            guard let lastReading else { return newReading }
217            return self
218                .lowPassFilterFactor * lastReading +
219                (1.0 - self.lowPassFilterFactor) * newReading
220        }
221    }
222
223    struct Axis: Hashable {
224        var axisName: String
225        var measurements: [Double] = []
226        var peaks = 0
227        var updatesSinceLastPeakCount = 0
228
229/// I love sets, like, a lot. Enough that when I first thought "but what's an *elegant* way to know when it's a
230/// good time to count the peaks again?" I thought of a one-liner set intersection, very semantic, very accurate to the
231/// underlying question of freshness of sensor data, and it made me happy, and I smiled.
232/// Anyway, a counter does the same thing with a 0s execution time, here's one of those:
233
234        mutating func shouldCountPeaks()
235            -> Bool { // Peaks are only counted once a second
236            updatesSinceLastPeakCount += 1
237            if updatesSinceLastPeakCount == MotionManager.updatesPerSecond {
238                updatesSinceLastPeakCount = 0
239                return true
240            }
241            return false
242        }
243    }
244
245    @Published var sensors: [ThreeAxisReadings] = []
246
247    // MARK: Private
248
249    private enum SensorNames: String, CaseIterable {
250        case attitude = "Attitude"
251        case rotationRate = "Rotation Rate"
252        case gravity = "Gravity"
253        case userAcceleration = "User Acceleration"
254        case acceleration = "Acceleration"
255        case gyroscope = "Gyroscope"
256        case magnetometer = "Magnetometer"
257    }
258
259    private static let updatesPerSecond: Int = 30
260
261    private let motionQueue = OperationQueue() // Don't read sensors on main
262
263    private let secondsToShow = 5 // Time window to observe
264    private let sensorUpdateInterval = 1.0 / Double(updatesPerSecond)
265    private let manager: CMMotionManager
266
267    private func startDeviceUpdates(manager _: CMMotionManager) {
268        self.manager
269            .startDeviceMotionUpdates(to: motionQueue) { motion, error in
270                self.collectReadings(motion, error)
271            }
272        self.manager
273            .startAccelerometerUpdates(to: motionQueue) { motion, error in
274                self.collectReadings(motion, error)
275            }
276        self.manager.startGyroUpdates(to: motionQueue) { motion, error in
277            self.collectReadings(motion, error)
278        }
279        self.manager
280            .startMagnetometerUpdates(to: motionQueue) { motion, error in
281                self.collectReadings(motion, error)
282            }
283    }
284
285    private func collectReadings(_ motion: CMLogItem?, _ error: Error?) {
286        DispatchQueue.main.async { // Add new readings on main
287            switch motion {
288            case let motion as CMDeviceMotion:
289                self.appendReadings(
290                    [motion.attitude.pitch, motion.attitude.roll,
291                     motion.attitude.yaw],
292                    to: &self.sensors[SensorNames.attitude.index()])
293                self.appendReadings(
294                    [motion.rotationRate.x, motion.rotationRate.y,
295                     motion.rotationRate.z],
296                    to: &self.sensors[SensorNames.rotationRate.index()])
297                self.appendReadings(
298                    [motion.gravity.x, motion.gravity.y, motion.gravity.z],
299                    to: &self.sensors[SensorNames.gravity.index()])
300                self.appendReadings(
301                    [motion.userAcceleration.x, motion.userAcceleration.y,
302                     motion.userAcceleration.z],
303                    to: &self.sensors[SensorNames.userAcceleration.index()])
304            case let motion as CMAccelerometerData:
305                self.appendReadings(
306                    [motion.acceleration.x, motion.acceleration.y,
307                     motion.acceleration.z],
308                    to: &self.sensors[SensorNames.acceleration.index()])
309            case let motion as CMGyroData:
310                self.appendReadings(
311                    [motion.rotationRate.x, motion.rotationRate.y,
312                     motion.rotationRate.z],
313                    to: &self.sensors[SensorNames.gyroscope.index()])
314            case let motion as CMMagnetometerData:
315                self.appendReadings(
316                    [motion.magneticField.x, motion.magneticField.y,
317                     motion.magneticField.z],
318                    to: &self.sensors[SensorNames.magnetometer.index()])
319            default:
320                print(error != nil ? "Error: \(String(describing: error))" :
321                    "Unknown device")
322            }
323        }
324    }
325
326    private func appendReadings(
327        _ newReadings: [Double],
328        to threeAxisReadings: inout ThreeAxisReadings) {
329        for index in 0 ..< threeAxisReadings.axes
330            .count { // For each of the axes
331            var axis = threeAxisReadings.axes[index]
332            let newReading = newReadings[index]
333
334            axis.measurements
335                .append(threeAxisReadings
336                    .applyFilter ? // Append new reading, as-is or filtered
337                    threeAxisReadings.lowPassFilter(
338                        lastReading: axis.measurements.last,
339                        newReading: newReading) : newReading)
340
341            if threeAxisReadings.applyFilter,
342               axis
343               .shouldCountPeaks() {
344                // And occasionally count peaks if filtering
345                axis.peaks = countPeaks(
346                    in: axis.measurements,
347                    quantizeFactor: threeAxisReadings.quantizeFactor)
348            }
349
350            if axis.measurements
351                .count >=
352                Int(1.0 / self
353                    .sensorUpdateInterval * Double(self.secondsToShow)) {
354                axis.measurements
355                    .removeFirst() // trim old data to keep our moving window representing secondsToShow
356            }
357            threeAxisReadings.axes[index] = axis
358        }
359    }
360
361    private func countPeaks(
362        in readings: [Double],
363        quantizeFactor: Double) -> Int { // Count local maxima
364        let quantizedreadings = readings.map { Int($0 * quantizeFactor) }
365        // Quantize into small Ints (instead of extremely small Doubles) to remove detail from little component waves
366
367        var ascendingWave = true
368        var numberOfPeaks = 0
369        var lastReading = 0
370
371        for reading in quantizedreadings {
372            if ascendingWave == true,
373               lastReading >
374               reading { // If we were going up but it stopped being true,
375                numberOfPeaks += 1 // we just passed a peak,
376                ascendingWave = false // and we're going down.
377            } else if lastReading <
378                reading {
379                // If we just started to or continue to go up, note we're ascending.
380                ascendingWave = true
381            }
382            lastReading = reading
383        }
384        return numberOfPeaks
385    }
386}
387
388extension CaseIterable where Self: Equatable {
389    func index() -> Self.AllCases
390        .Index {
391        // Force-unwrap of index of enum case in CaseIterable always succeeds.
392        return Self.allCases.firstIndex(of: self)!
393    }
394}
395
396typealias Sensor = MotionManager.ThreeAxisReadings

下面是完成后的原型。运行良好,可以看到,对于踏板动作没有反应的传感器都被关掉了,只需要查看有关系的三个传感器即可。我关掉了前两个,因为我觉得单车的波形并不是很清晰。但最后一个我可以在Z轴上清晰地看到运动。所以,我打开了低通滤波器,然后将其量化成飞航达的数字。这样就能精确地计算出踩踏板的次数。

完整的代码,请参见GitHub:https://github.com/Halle/StationaryBikeStepCounter/blob/main/ContentView.swift。

作者:Halle Winkler

译者:CSDN - 弯月

原文:https://theoffcuts.org/posts/prototyping-a-stationary-bike-stepper/

-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

df133401bb9dde20313d31875bf62e99.png

点击👆卡片,关注后回复【面试题】即可获取

在看点这里3a22bfaccb09c7a05a6fe83a6291d3e7.gif好文分享给更多人↓↓

这篇关于“我用 400 行 Swift 代码给破旧的自行车加了一个动感单车计步器!”的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/235529

相关文章

python实现pdf转word和excel的示例代码

《python实现pdf转word和excel的示例代码》本文主要介绍了python实现pdf转word和excel的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录一、引言二、python编程1,PDF转Word2,PDF转Excel三、前端页面效果展示总结一

在MyBatis的XML映射文件中<trim>元素所有场景下的完整使用示例代码

《在MyBatis的XML映射文件中<trim>元素所有场景下的完整使用示例代码》在MyBatis的XML映射文件中,trim元素用于动态添加SQL语句的一部分,处理前缀、后缀及多余的逗号或连接符,示... 在MyBATis的XML映射文件中,<trim>元素用于动态地添加SQL语句的一部分,例如SET或W

使用C#代码计算数学表达式实例

《使用C#代码计算数学表达式实例》这段文字主要讲述了如何使用C#语言来计算数学表达式,该程序通过使用Dictionary保存变量,定义了运算符优先级,并实现了EvaluateExpression方法来... 目录C#代码计算数学表达式该方法很长,因此我将分段描述下面的代码片段显示了下一步以下代码显示该方法如

python多进程实现数据共享的示例代码

《python多进程实现数据共享的示例代码》本文介绍了Python中多进程实现数据共享的方法,包括使用multiprocessing模块和manager模块这两种方法,具有一定的参考价值,感兴趣的可以... 目录背景进程、进程创建进程间通信 进程间共享数据共享list实践背景 安卓ui自动化框架,使用的是

SpringBoot生成和操作PDF的代码详解

《SpringBoot生成和操作PDF的代码详解》本文主要介绍了在SpringBoot项目下,通过代码和操作步骤,详细的介绍了如何操作PDF,希望可以帮助到准备通过JAVA操作PDF的你,项目框架用的... 目录本文简介PDF文件简介代码实现PDF操作基于PDF模板生成,并下载完全基于代码生成,并保存合并P

SpringBoot基于MyBatis-Plus实现Lambda Query查询的示例代码

《SpringBoot基于MyBatis-Plus实现LambdaQuery查询的示例代码》MyBatis-Plus是MyBatis的增强工具,简化了数据库操作,并提高了开发效率,它提供了多种查询方... 目录引言基础环境配置依赖配置(Maven)application.yml 配置表结构设计demo_st

SpringCloud集成AlloyDB的示例代码

《SpringCloud集成AlloyDB的示例代码》AlloyDB是GoogleCloud提供的一种高度可扩展、强性能的关系型数据库服务,它兼容PostgreSQL,并提供了更快的查询性能... 目录1.AlloyDBjavascript是什么?AlloyDB 的工作原理2.搭建测试环境3.代码工程1.

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

Java中ArrayList的8种浅拷贝方式示例代码

《Java中ArrayList的8种浅拷贝方式示例代码》:本文主要介绍Java中ArrayList的8种浅拷贝方式的相关资料,讲解了Java中ArrayList的浅拷贝概念,并详细分享了八种实现浅... 目录引言什么是浅拷贝?ArrayList 浅拷贝的重要性方法一:使用构造函数方法二:使用 addAll(

JAVA利用顺序表实现“杨辉三角”的思路及代码示例

《JAVA利用顺序表实现“杨辉三角”的思路及代码示例》杨辉三角形是中国古代数学的杰出研究成果之一,是我国北宋数学家贾宪于1050年首先发现并使用的,:本文主要介绍JAVA利用顺序表实现杨辉三角的思... 目录一:“杨辉三角”题目链接二:题解代码:三:题解思路:总结一:“杨辉三角”题目链接题目链接:点击这里