“我用 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调试串口的示例代码

《利用Python调试串口的示例代码》在嵌入式开发、物联网设备调试过程中,串口通信是最基础的调试手段本文将带你用Python+ttkbootstrap打造一款高颜值、多功能的串口调试助手,需要的可以了... 目录概述:为什么需要专业的串口调试工具项目架构设计1.1 技术栈选型1.2 关键类说明1.3 线程模

Python Transformers库(NLP处理库)案例代码讲解

《PythonTransformers库(NLP处理库)案例代码讲解》本文介绍transformers库的全面讲解,包含基础知识、高级用法、案例代码及学习路径,内容经过组织,适合不同阶段的学习者,对... 目录一、基础知识1. Transformers 库简介2. 安装与环境配置3. 快速上手示例二、核心模

Java的栈与队列实现代码解析

《Java的栈与队列实现代码解析》栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶,栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等,本文给大家介绍J... 目录栈的概念(Stack)栈的实现代码队列(Queue)模拟实现队列(双链表实现)循环队列(循环数组

使用Java将DOCX文档解析为Markdown文档的代码实现

《使用Java将DOCX文档解析为Markdown文档的代码实现》在现代文档处理中,Markdown(MD)因其简洁的语法和良好的可读性,逐渐成为开发者、技术写作者和内容创作者的首选格式,然而,许多文... 目录引言1. 工具和库介绍2. 安装依赖库3. 使用Apache POI解析DOCX文档4. 将解析

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

使用Python实现全能手机虚拟键盘的示例代码

《使用Python实现全能手机虚拟键盘的示例代码》在数字化办公时代,你是否遇到过这样的场景:会议室投影电脑突然键盘失灵、躺在沙发上想远程控制书房电脑、或者需要给长辈远程协助操作?今天我要分享的Pyth... 目录一、项目概述:不止于键盘的远程控制方案1.1 创新价值1.2 技术栈全景二、需求实现步骤一、需求

Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码

《Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码》:本文主要介绍Java中日期时间转换的多种方法,包括将Date转换为LocalD... 目录一、Date转LocalDateTime二、Date转LocalDate三、LocalDateTim

jupyter代码块没有运行图标的解决方案

《jupyter代码块没有运行图标的解决方案》:本文主要介绍jupyter代码块没有运行图标的解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录jupyter代码块没有运行图标的解决1.找到Jupyter notebook的系统配置文件2.这时候一般会搜索到

Python通过模块化开发优化代码的技巧分享

《Python通过模块化开发优化代码的技巧分享》模块化开发就是把代码拆成一个个“零件”,该封装封装,该拆分拆分,下面小编就来和大家简单聊聊python如何用模块化开发进行代码优化吧... 目录什么是模块化开发如何拆分代码改进版:拆分成模块让模块更强大:使用 __init__.py你一定会遇到的问题模www.

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La