首页 >>  正文

免费计步器

来源:baiyundou.net   日期:2024-07-08

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

原文链接: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 代码给破旧的自行车加了一个动感单车计步器!”

阳冯肾2955计步器软件哪个好用 -
茹丽超17837216649 ______ 动动计步器是一款能够记录人的健康数据运动应用,它无需外设,只需手机自带的内置传感器即可准确记录数据.百度:当下软件园动动计步器 即可

阳冯肾2955散步计步软件如何下载 -
茹丽超17837216649 ______ 百度散步计步器app,然后找到下载,或者在360手机助手里面搜散步计步器.或者在豌豆荚中找

阳冯肾2955手机计步下什么软件好 -
茹丽超17837216649 ______ 我用过一个是动动计步器 是在应用宝里下载的,还差不多吧 挺好用的,而且这个不用联网,不费流量 界面很简约的,而且可以全面看你之前的数据 还可以分年月日的来查看 我觉得安卓上还是挺好的一个健康类的软件呢 不过如果你有手环什么的话可以用配套的软件 效果会更好一些的,希望可以帮到你哦

阳冯肾2955何种计步器手机软件好? -
茹丽超17837216649 ______ 计步器的话是有很多种的,这种锻炼类的软件随着人们越来越重视自身的健康问题而层出不穷 我用过乐动力,感觉还不错,是专门的计步类的 不过还有咕咚什么的,除了计步还有其他的更好玩的锻炼功能 如果只计步的话,还有什么春雨计步器之类的,都是很多的 你在应用宝的搜索栏输入“计步”,就会出现这些了 上面会显示具体介绍,你再看你是要只计步,还是需要更多的锻炼

阳冯肾2955走路计步器那种软件好 -
茹丽超17837216649 ______ 咕咚、悦动圈、嗖嗖溜达、平安好医生、春雨医生这些软件的走路功能都不一样. 不同的软件都是结合自己的产品来附加各种功能,你可以尝试下

阳冯肾2955怎么下一个走一走计步器 -
茹丽超17837216649 ______ 近几年来手机计步器软件变得越来越流行了,人们可以不用另外购买计步器,通过手机软件就可以进行计步,平时运动起来很方便,一边手机听歌,一边计步.不仅有利于身体健康,还可以减肥.可上网下一个叫:春雨计步器是一款记录每天运动量的免费应用程序,支持记录每日行走步数,自动测量卡路里消耗,步行高低峰时段实时显示,同时还能与好友PK步数.

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

阳冯肾2955电子计步器怎么用 -
茹丽超17837216649 ______ 动动计步器是世界排名第一的拥有千万用户的手机计步器Pacer的安卓中文版,不用额外的硬件设备,无需连接网络,无需开启GPS,省电省流量.报表和分析功能,运动量直观展现,群组功能让你找到志同道合的小伙伴,说不定有妹子.那下...

阳冯肾29550用什么计步器安卓2?安卓2.0用什么计步器
茹丽超17837216649 ______ 咕咚,悦动,春雨计步器

阳冯肾2955淘宝计步器价格 一般多少 -
茹丽超17837216649 ______ 一般从2块到900都有,就看你需要什么类型的啦.30以内的一般都是摆锤式的计步器,属于机械类2D的,只能识别前后运动.100元左右的是3D的,可以识别三维的运动.200以上的有些是品牌的,有些...

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