首页 >>  正文

下载计步器

来源:baiyundou.net   日期:2024-09-05

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

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

声明:本文为 CSDN 翻译,未经允许不可转载。

作者 | Halle Winkler译者 | 弯月 责编 | 屠敏

出品 | CSDN(ID:CSDNnews)

以下为译文:

最近我跟朋友聊起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.

3

4import Charts

5import CoreMotion

6import SwiftUI

7

8// MARK: - ContentView

9

10/// ContentView is a collection of motion sensor UIs and a method of calling back to the model.

11

12struct ContentView {

13 @ObservedObject var manager: MotionManager

14}

15

16extension ContentView: View {

17 var body: some View {

18 VStack {

19 ForEach(manager.sensors, id: \\.sensorName) { sensor in

20 SensorChart(sensor: sensor) { applyFilter, lowPassFilterFactor, quantizeFactor in

21 manager.updateFilteringFor(

22 sensor: sensor,

23 applyFilter: applyFilter,

24 lowPassFilterFactor: lowPassFilterFactor,

25 quantizeFactor: quantizeFactor)

26 }

27 }

28 }.padding([.leading, .trailing], 6)

29 }

30}

31

32// MARK: - SensorChart

33

34/// I like to compose SwiftUI interfaces out of many small modules. But, there is a tension when it's a

35/// small UI overall, and the modules will each have overhead from propagating state, binding and callbacks.

36

37struct SensorChart {

38 @State private var chartIsVisible = true

39 @State private var breakOutAxes = false

40 @State private var applyingFilter = false

41 @State private var lowPassFilterFactor: Double = 0.75

42 @State private var quantizeFactor: Double = 50

43 var sensor: Sensor

44 let updateFiltering: (Bool, Double, Double) -> Void

45 private func toggleFiltering() {

46 applyingFilter.toggle()

47 updateFiltering(applyingFilter, lowPassFilterFactor, quantizeFactor)

48 }

49}

50

51extension 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.

54

55 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 }

75

76/// Sensor charts, either one chart with three axes, or three charts with one axis. I love how concise Swift Charts can be.

77

78 if chartIsVisible {

79 if breakOutAxes {

80 ForEach(sensor.axes, id: \\.axisName) { series in

81 // Iterate charts from series

82 Chart {

83 ForEach(

84 Array(series.measurements.enumerated()),

85 id: \\.offset) { index, datum in

86 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。

","force_purephv":"0","gnid":"9336715eb16817adf","img_data":[{"flag":2,"img":[{"desc":"","height":"80","s_url":"https://p0.ssl.img.360kuai.com/t0186957a1ca5352752_1.gif","title":"","url":"https://p0.ssl.img.360kuai.com/t0186957a1ca5352752.gif","width":"640"}]}],"original":0,"pat":"art_src_1,fts0,sts0","powerby":"hbase","pub_time":1659507726000,"pure":"","rawurl":"http://zm.news.so.com/30e35a02b9800da9e8416d385e7a45e4","redirect":0,"rptid":"e454df35b36c4d42","s":"t","src":"CSDN","tag":[{"clk":"ktechnology_1:动感单车","k":"动感单车","u":""},{"clk":"ktechnology_1:ts","k":"ts","u":""}],"title":"“我用 400 行 Swift 代码给破旧的自行车加了一个动感单车计步器!”

杨须孙1012各位有没有好用的跑步计步器啊?各位有没有好用的跑步计步器啊!iP
终永胆18755943897 ______ Nike+和UltimatePedometer是世界上比较流行的iphone手机运动计步软件,但是中文汉化做得比较差,GPS定位国内有漂移.目前国内做的比较好的iphone运动软件是1)咕咚运动(免费GPS运动定位加上额外卖的无线上传数据的计步器),2)慢点生活计步器 (免费的iphone手机计步器和GPS定位).如果你觉得我的建议还不错,麻烦你能够点击一下“有用”这个选项给我加分,(已谢的话是没有加分的,拜托了),无需你任何事物,只是你在对我的回答作为一种肯定和鼓励,谢谢~~

杨须孙1012请问oppo手机怎么使用计步器? -
终永胆18755943897 ______ 1、oppo手机内置运动计步器,适配了微信运动,时刻记录你的运动脚步,还可以与你的朋友PK运动量,实时分享点赞.将计步器加入自启动管理,并添加到纯净后台运行.这样,手机就会自动帮你记步数. 2、下载相关的计步器软件. 下载乐...

杨须孙1012安卓4.1.2用什么计步器软件好 -
终永胆18755943897 ______ 安卓系统的话使用计步器,其实qq和微信都是自带的,如果想要下载软件的话,可以在应用宝上进行查找,都是安卓系统可以使用的,兼容性比较强,可以在通过简介后直接进行下载使用,一般像是春雨计步器啥的都是能够进行下载的.

杨须孙1012计步器在哪里找
终永胆18755943897 ______ 楼主您好: 根据您问题的描述,首先肯定的说手机上的这个计步器您找不到,它是通过传感器(名字叫三轴加速度传感器)来实现记步的目的的(原理是它可以感测到手机三个不同方向传来的加速度,通过这个加速度值进行科学运算,人走路的...

杨须孙1012怎样在手机屏幕上显示步数 -
终永胆18755943897 ______ 怎样在手机桌面上显示步数1. 使用第三方应用如果您想要在手机桌面上方便地查看自己的步数,可以通过下载第三方应用实现.例如,您可以下载类似于“健康计步器”这样的应用程序,并在应用内启用“桌面小部件”功能.这样,您就可以在...

杨须孙1012什么样手机可以下载计步器 -
终永胆18755943897 ______ 你好,你可以到应用宝上搜索,找用户数量最多的下载安装,就很好,应用宝是专业的手机软件资源下载平台.上面有很丰富的软件资源可供下载安装.应用宝的兼容性非常好,可以和很多同类型的软件实现完美兼容. 使用应用宝不仅可以免费下载到丰富的安卓软件、游戏、音乐、视频、电子书、壁纸等资源;还可以便捷地进行手机优化、应用检测、一键Root、资料备份等操作. 望采纳,谢谢

杨须孙1012手机不计步 -
终永胆18755943897 ______ 小伙伴如果想在手机中安装计步器,可以在软件商店--下载乐动力、悦跑圈跑步、春雨计步器等软件,来显示日常的步数; 温馨提示:计步器需要保持在后台运行才可以收集日常的运动数据,若从后台关闭就无法收集,建议可以将计步器加入自启动管理并且添加到纯净后台运行哦.

杨须孙1012走路计步器那种软件好 -
终永胆18755943897 ______ 咕咚、悦动圈、嗖嗖溜达、平安好医生、春雨医生这些软件的走路功能都不一样. 不同的软件都是结合自己的产品来附加各种功能,你可以尝试下

杨须孙1012我华为手机计步器不见了怎么找回 -
终永胆18755943897 ______ 手机预装应用软件随着升级有所不同的,一些应用的合约到期了,下次升级就没有它了. 自己去下载一个就是了.如果对之前的计步器很怀旧,网上找不到,就找一个有这款计步器的手机把计步器导出到电脑,然后再装你手机里就行.

杨须孙1012安卓手机如何设置微信步数 -
终永胆18755943897 ______ 1.首先先下载安卓【春雨计步器】,如果手机商城下载不了,可以到搜狗浏览器搜索下载.2.打开【春雨计步器】点击右下方的【更多】选项然后点击【微信登录】.3.如果是有两个微信的,可以如图所示进行选择你所需要修改步数的微信.4....

(编辑:自媒体)
关于我们 | 客户服务 | 服务条款 | 联系我们 | 免责声明 | 网站地图 @ 白云都 2024