Trip Data Logging -- Phaserunner, CA, & GPS

Joined
Sep 8, 2019
Messages
659
Location
USA, CA, Bay Area
I would like a device that logs "ALL THE THINGS" for a PR/CA setup.

What I Want

A small device I can add to the a bike which will connect to the CycleAnalyst, the Phaserunner, and a GPS receiever and create a combined log of the data from all three. I would like the device to have a WiFi adapter and be able to host a simple webapp which can manage the on-device logs (ie: hit up http://192.168.../ and download/delete logs). I wish to use this data for ride/bike analytics and to sync up to other recording devices (action cameras with their own GPS data) for ride telemetry.

What I've Found So Far

On the market today, certainly, is the Grin offered Analogger, but there are several issues with it. 1) The logs aren't timestamped; this makes using the data later on for overlays a major hassle. 2) The GPS data is logged separately in a format that, while a standard, doesn't carry the bike data along with it. Thus, 3) you can only get a combined file of the data by using Grin's online app to merge them (ok, maybe that can be done locally, but more steps!). Finally, and most importantly, it doesn't log any data from the Phaserunner, in which I think there are some interesting tidbits which would be great to have.

There looks to have been an effort, several years ago, to do more-or-less exactly what I'm after with this software I found called the cycle_analyst_logger. Sadly, the software seems quite out of date and I can't get it running. (It's got a long dependency chain of things built for ruby 2, and it seems a current version of ruby is 3 -- I don't know enough ruby to slog through trying to get everything upgraded.)

There are also some more purpose-oriented solutions, like the SparkFun OpenLog Artemis which is a bit of a one-stop shop for getting various sensor data combined into a single log file, but it seems to only support a single serial connection, and there are no adapters to "extend" that to more. It also doesn't offer that http endpoint, so getting data off it is the whole "pop out the sd card" routine I'd like to avoid.

I do have a RPI4, and spent the afternoon re-installing the OS and got it all configured and installed so I can stream stats from the CA -- heck, with a little CLI trickery, I can even solve the "no timestamps issue" just cat /dev/ttyUSB0 | grep --line-buffered -v '^$' | ts "%Y-%m-%dT%H:%M:%.S%z". Still, no phaserunner stats and no GPS data, though. (It's a shame the PR doesn't have the same simple "stream data out" setup as the CA, but after seeing the list of available information, I can understand why this isn't really feasible.)

I have a few ESP32's sitting around, so that's an option as well. Apparently they can have up to 3 hardware serial channels going, which is good since you'd need one for each of the CA, PR, and GPS. The rpi, though, seems a little friendlier with it's full OS ecosystem and the device isn't physically huge. Almost without a doubt, the esp32 setup would use less power, but on an ebike this is only on while in motion, so I don't see it being a super huge issue.

What I Could Use Advice On

I guess I'm closest with the rPi at this point (it seems adding a wifi dongle and a usb gps is, actually, pretty easy). So, given that, the big hurdle I'm facing is getting data from the phaserunner. Any sage pointer on how to do that?

Alternatively, are there other solutions that I've missed which get me a lot closer?
 
Well, after a deep dive into the 70's tech (why the heck is modbus got coils, contacts, and registers!?!?) I was able to derive a very simple python script with minimalmodbus and based on these definitions and "scale" factors get usable data from the phaserunner!

Python:
#!/usr/bin/env python3
import minimalmodbus
import time

instrument = minimalmodbus.Instrument('/dev/ttyUSB0', 1)  # port name, slave address (in decimal)

instrument.serial.baudrate = 115200

stats = [
 {'name': "Battery Voltage", 'address': 265, 'decimals': 0, 'signed': True, 'scale': 32},
 {'name': "Battery Current", 'address': 266, 'decimals': 0, 'signed': True, 'scale': 32},
 {'name': "Throttle Voltage", 'address': 270, 'decimals': 0, 'signed': True, 'scale': 4096},
 {'name': "Phase A Current", 'address': 282, 'decimals': 0, 'signed': True, 'scale': 32},
 {'name': "Phase B Current", 'address': 283, 'decimals': 0, 'signed': True, 'scale': 32},
 {'name': "Phase C Current", 'address': 284, 'decimals': 0, 'signed': True, 'scale': 32},
 {'name': "Phase A Voltage", 'address': 285, 'decimals': 0, 'signed': True, 'scale': 32},
 {'name': "Phase B Voltage", 'address': 286, 'decimals': 0, 'signed': True, 'scale': 32},
 {'name': "Phase C Voltage", 'address': 287, 'decimals': 0, 'signed': True, 'scale': 32},
 {'name': "Wheel RPM (Speed Sensor Based)", 'address': 311, 'decimals': 0, 'signed': True, 'scale': 16},
 {'name': "Wheel RPM (Motor Based)", 'address': 312, 'decimals': 0, 'signed': True, 'scale': 16},
 {'name': "Measured Wheel RPM", 'address': 313, 'decimals': 0, 'signed': True, 'scale': 16},
]


while True:
  for i in stats:
    val = instrument.read_register(i['address'], i['decimals'], signed=i['signed'])
    print(i['name'], val / i['scale'])
  time.sleep(1)

My python is a bit rusty (hence me not quire recalling how to use the nicer dict dot syntax, I'll look it up later), but it *does* work:

Code:
Battery Voltage 84.21875
Battery Current 0.0
Throttle Voltage 0.05126953125
Phase A Current -0.15625
Phase B Current -0.125
Phase C Current 0.15625
Phase A Voltage 43.5
Phase B Voltage 42.875
Phase C Voltage 42.5625
Wheel RPM (Speed Sensor Based) 0.0
Wheel RPM (Motor Based) 18.9375
Measured Wheel RPM 19.5625

Battery Voltage 84.21875
Battery Current 0.0
Throttle Voltage 0.053955078125
Phase A Current -0.21875
Phase B Current -0.0625
Phase C Current 0.21875
Phase A Voltage 43.75
Phase B Voltage 42.09375
Phase C Voltage 43.75
Wheel RPM (Speed Sensor Based) 0.0
Wheel RPM (Motor Based) 29.0
Measured Wheel RPM 29.375

Battery Voltage 84.21875
Battery Current 0.0
Throttle Voltage 0.052734375
Phase A Current 0.03125
Phase B Current 0.0625
Phase C Current 0.15625
Phase A Voltage 42.5625
Phase B Voltage 42.6875
Phase C Voltage 43.625
Wheel RPM (Speed Sensor Based) 0.0
Wheel RPM (Motor Based) 32.6875
Measured Wheel RPM 33.375

(I was spinning the wheel by hand)

I ordered a GPS unit and wifi adapter to hook up to the rPi and we'll see if I can hack together something to read all the tty's and smash the data together into some kind of csv'ish thing. Then a webserver with some basic management functions -- have to dig out the Flash knowledge from the depths of my brain.
 
The dockerfile for this is super simple:

Code:
FROM python:3
RUN pip install --no-cache-dir minimalmodbus

And to start it, you need to bind the TTY port through, like this:

Code:
docker run -ti --rm -v ./:/app --device=/dev/ttyUSB0 -w /app bikelogger bash

(I'm executing it in an interactive mode here so I can run "python app.py" or whatever you call the script above. I'm also mounting in the current working directory since the container is ephemeral.)

I turned to docker for this because I can't stand installing deps on the primary OS -- it's such a muddled mess.
 
Got a lot further on this today -- PR, CA, and GPS are all logging to a single file. I'm waiting on the GPS unit to show up so I can integrate that instead of the current dummy data, but everything I've researched it seems like it'll be pretty easy.

Did setup a github repo for this, however: GitHub - chuyskywalker/grin-logger: Phaserunner and Cycle Analyst Logging

Still a good chunk of TODO's in the code as well as error handling and the whole "build a webui" thing, but I've made really good progress.
 
Got the GPS (no frills, Amazon). Plugs right in, immediately showed up, and was pretty simple to immediately start gathering the NMEA data, parsing it, and grabbing out the datetime,lat,lon and a few other fun goodies.

I added better error handling that deals with USB devices coming and going (and not present on script start). The log files now get created with a unixtimestamp for easy sorting based on the GPS fix.

I also realized I can skip the whole webserver management thing -- because I just created a SMB share on the rPi for the app and logs. I can manage them both from a share on my desktop pc easily enough.

Next Steps: I need to configure the the wireless adapter so it'll connect to my network. I also need to setup the rpi to automatically start this container at boot. Then I stick it all on the bike with a temporary powerbank and get out for a quick ride to see how well it performs -- or if I come back to a log of garbage :D
 
Bit of an problem. It seems on the rPi4 (specifically) if there is any power backfeeding from the USB ports, the device will not boot up. Trying each USB plug individiually (pr, ca, gps), I've confirmed that when the PR is plugged in, it's backfeeding and I can't boot the rpi. This actually makes sense as the PR is the only device which is actually a TRRS with the intention of feeding 5v on the line to, in theory, power a BT dongle someday down the line.

This is a problem for me, because I'd have to boot the computer before turning the PR on -- but I use the PR to trigger my dcdc converters on the bike, the ones which power the rPi. (Key turns on PR, PR pas 12v energizes, relay from the prpas12 closes, battery voltage on other side of relay goes to dcdc converters, including one which takes BatV down to 5v for the rPi, rPi boots.)

Hopefully one of these usb isolator / power stoppers I'm taking a swing at trying can solve the issue.
 
First sampling went...pretty well. I've learned a few things looking at what gets logged though:
  1. Lots of duplicate data
    With a first go-around, I didn't want to lose anything, so I just double/tripled logged things. Now that I have some data, you'll see in these notes that several items are duplicative. I'll clean those out.
  2. Regen not being interpreted correctly for "Battery Power (W)" (getting a negative rollover, I'm guessing. Needs to be signed?)
  3. motor speed %
    in reverse, I think, goes wild -- several entries with 1500%
    routinely goes over 100%; with the reversing above showing up as 1500% ish.
    I suspect I'll just drop motor speed %; I thought it might be interesting, but probably not in an ebike context.
  4. "Wheel Speed Motor Based (RPM)" and "Measured Wheel Speed (RPM)" are redundant -- usually within 5rpm of each other; likely just due to measurement timing difference
  5. rounding factor could use to be added to many of the values (I don't think we need voltage beyond 0.000v accuracy, and frankly I doubt it's actually that accurate); add a generic 'round' parameter
  6. drop phase current/voltage -- it really doesn't sample fast enough to show anything useful
  7. interestingly; when you are running regen the "motor current" is always positive, but the battery current does go negative
  8. the CA amps and PR amps are identical; could drop one (like the PR since that's a call response loop)
  9. pr voltage is reported a smidge higher, but again, mirrors the CA data (drop PR data)
  10. speed from the CA is...wrong?
    oddly, while riding on the fast section, I did see the display show 40mph (~65kph) but the ca data says we maxed out at ~40kph... oh, it's recording mph, not kph. Yeah, converting all the values to KPH, they match up to the PR. Thus, the CA records speed in whatever units the device is set to; this makes logging that value a bit...unreliable
    On the one hand, the PR is consistent (always kph), but it also means that it won't necessarily match up with the CA if the wheel sizes haven't been perfectly matched up. And, people will likely only fine tune the CA wheel size since that's what they see...perhaps it'll just have to be a note/variable that folks can set "do you run the CA in kph or mph?" kind of thing
  11. ca distance seems to be cumulative (maybe for the current trip?) either way, it's not terribly useful since the GPS points will end up calculating distance close enough
I realized I'd also really like some kind of display on the little mini computer. Would like to know that the logger is running, number of GPS satellites, and some other bits of data. Would provide a level of assurance that the device is working as intended.

I also found that if the GPS drops out, the data log continues, but there are no timestamps available. In theory, it might bet better to use the RCM gps messages to get the datetime for an initial start/timesync and then use the system time to log timestamps. Doing this would ensure that, even if the GPS gets fuzzed out, the logfile timestamps and data will still be accurate. Overlays of this may end up having a "map jump" from fuzz point in/out, but the ride data (amps, cadence, etc) would be accurately displayed still.

I'll see how the data syncs up to the telemetry overlay later and start paring down the script and what it records.

Hopefully in the next day or two I should have a video example of this up-and-running.
 
Sheesh. GPSD, NTP, Chrony, etc, etc. Trying to get GPS-to-time at a system level is...quite a mess. Especially as most of these systems are very...shall we say, pedantic? Without the uber-accurate PPS signal, they basic say NEIN IS NO GUT FAIL because being more than 5ms off actual time is inconceivable.

Anyway, I disabled the systems clock and just date -u --set={} -- it's a dirty hack and can have as much as 1 second off accurate! I can live with that, lol. I suspect pretty much everyone can :)

But all this hulabulo does mean that I can record timestamps via the system clock instead of the receive GPS data during a recording loop -- thus is GPS drops off, the other log records still have an accurate timestamp.
 
display-on-bike.jpg

All mounted up with a little 3d printed case that has some rudimentary gopro style mounts.

I imagine, if you wanted to go a bit further, you could replace the CA screen with something like this. But it's getting a bit "out there" to go so far.

I'm now in that "last 20% is 80% of the work" phase:
  • extend the display wire so it can reach the electronics bag
  • run the display wire through the internal tube
  • run the CA wire through the tube
  • tuck the rpi and dcdc for the pi into the electronics bag
I'm dreading these last few steps because a) the tube is STUFFED with wires already, I'm really not sure I can get two more through there -- especially with the connectors these will have and b) the gear bag holding all my electronic extras is already very full. I may wait until I have a bigger bag to attempt all of this.
 
Back
Top