首页 >>  正文

免费计步软件哪个好

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

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

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

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

权剂忽1620手机计步器软件哪个好 -
别往宝19184795102 ______ 你好,春雨计步器比较好, 在应用宝下载就可以了, 它是安卓软件的一个下载平台,也是有电脑版的啊. 并且这里是有软件、游戏等分类的, 这样就可以根据自己的兴趣爱好来下载了,还是比较耐玩一些的呢.

权剂忽1620现在哪个计步器手机APP最好用?用户最多? -
别往宝19184795102 ______ 咕咚,悦跑圈,虎扑跑步 这几个app都很火,可以计走路,也可以计跑步,还有各种赛事,很好很强大,推荐你用.

权剂忽1620计步软件哪个好 -
别往宝19184795102 ______ QQ中的计步器很好 计步器我用了的 都是只要你把手机晃一下它就会记步 QQ计步器不会 望采纳

权剂忽1620计步器app哪个好?计步器app哪个好
别往宝19184795102 ______ 计步器要好一点 毕竟价位便宜一点的哦~

权剂忽1620我手机不支持微信记步那什么,推荐一下,哪个软件,记步准确,非常好用的,软件! -
别往宝19184795102 ______ 软件是花钱装的

权剂忽1620什么计步软件APP比较好用和准确?
别往宝19184795102 ______ 我一般用的“耐克”的几部软件,有GPRS啊、室内室外等等,感觉很不错.

权剂忽1620跑步用什么手机软件好? -
别往宝19184795102 ______ 跑步的话,有乐动力啊,或者是咕咚啊 都是不错的健身软件,跑步的话也是很好用的 也有计步功能,要是有那种会显示自己跑步的路线图的话 可以使用悦动圈跑步,是最近比较火的 你可以在应用宝上直接搜索跑步,来进行选择下载 或者到软件——分类——健身中查找,会出现除了跑步也有其他功能的健身类的 这里的还是很齐全,你可以根据介绍进行选择

权剂忽1620有什么比较好的软件,可以计算我的走路步数? -
别往宝19184795102 ______ 手机下载一个计步器软件就可以了.、

权剂忽1620计步器软件哪个好 -
别往宝19184795102 ______ 你好,第三方软件有乐动力,moves等,iOS8系统自带的健康应用程序可以计步.还有什么疑问,可以继续追问!

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