MackeyStingray / o2r

GNU General Public License v3.0
18 stars 5 forks source link

Realtime Pleth / PPG data read #2

Open z3ugma opened 1 year ago

z3ugma commented 1 year ago

Hi there! I'd like to help implement realtime read of the PPG/Pleth signal (as per https://github.com/MackeyStingray/o2r/blob/master/README.md?plain=1#L57 ), and I'm wondering what experiments you've done toward this end.

My ultimate goal would be to be able to gather the PPG waveform in realtime:

image

I can put a few things I know!

You've implemented Commands 3,4,5,20,21,22,23,24 - are there other commands? How did you originally discover this list of commands?

CMD_INFO = 20 # 0x14 # 1, rx len = ~264, tx no data
CMD_PING = 21 # 0x15 # 0, rx len = 12, tx no data
CMD_CONFIG = 22 # 0x16 # 2, rx len = 12, tx JSON
CMD_READ_SENSORS = 23 # 0x17 # 3, rx len = 21, tx no data
CMD_FACTORY_DEFAULT = 24 # 0x18 # 0, rx len = 12, tx no data
CMD_FILE_OPEN = 3 # rx len = 12, tx filename length and filename
CMD_FILE_READ = 4 # tx block number
CMD_FILE_CLOSE = 5 # tx no data
#CMD_UNK = 255

Maybe some other command reads the realtime pleth waveform?

Let me know if you'd be interested in working on this together, or have any hints!

z3ugma commented 1 year ago

Continued sleuthing - in the ViHealth APK (Android app), this LepuBle Kotlin library is called. In this file - https://github.com/viatomEcho/LepuBle/blob/master/BLE_SDK_API.md

We can see references to oxyGetPpgRt and oxyGetWave that specify it can be done on O2Ring.

------------------------------------------------------------------------O2Ring,BabyO2-------------------------------------------------------------------

oxyGetPpgRt(model: Int) :获取实时原始数据,支持设备O2Ring,BabyO2
oxyGetWave(model: Int) :获取实时波形数据,支持设备O2Ring,BabyO2
oxyGetRtParam(model: Int) :获取实时参数值,支持设备O2Ring,BabyO2
updateSetting(model: Int, type: String, value: Any) :更新设备设置,支持设备O2Ring,BabyO2

and this commit: https://github.com/viatomEcho/LepuBle/commit/2db64621969b436fcef5da992bd8728d72513b10 looks like it modifies some ability to fetch PpgRT (ppg realtime?)

Here we see the list of commands, like your above public static int OXY_CMD_RT_PARAM_DATA = 0x17; -> 0x17 being the command to CMD_READ_SENSORS https://github.com/viatomEcho/LepuBle/commit/2db64621969b436fcef5da992bd8728d72513b10#diff-a48a1d358b8776c9470e3c4f7381072461b2a1eb28659c1352428b0f836d6210R22

    public static int OXY_CMD_INFO = 0x14; //20
    public static int OXY_CMD_PARA_SYNC = 0x16; //22
    public static int OXY_CMD_RT_DATA = 0x1B; //27
    public static int OXY_CMD_RT_PARAM_DATA = 0x17; //21
    public static int OXY_CMD_RESET = 0x18; //24
//File read 
    public static int OXY_CMD_READ_START = 0x03; //3
    public static int OXY_CMD_READ_CONTENT = 0x04; //4
    public static int OXY_CMD_READ_END = 0x05; //5

If so it appears that command 27 is the realtime data: public static int OXY_CMD_RT_DATA = 0x1B; //27

https://github.com/viatomEcho/LepuBle/blob/2db64621969b436fcef5da992bd8728d72513b10/blepro/src/main/java/com/lepu/blepro/ble/cmd/OxyBleCmd.java#L103

This function describes the command string


    public static byte[] getRtWave() {
        int len = 1;

        byte[] buf = new byte[8 + len];
        buf[0] = (byte) 0xAA; //aa
        buf[1] = (byte) OXY_CMD_RT_DATA; //1b
        buf[2] = (byte) ~OXY_CMD_RT_DATA; // ff-1b = e4
        buf[5] = (byte) len; //01
        buf[6] = (byte) (len >> 8);

        buf[7] = (byte) 0;  // 0 -> 125hz;  1-> 62.5hz

        buf[8] = BleCRC.calCRC8(buf);

        return buf;

    }

b'\xaa\x1b\xe4\x00\x00\x01\x00\x00\\<CRC>

^^ I'll try to implement the command for 1B

z3ugma commented 1 year ago

Additional findings: public static int OXY_CMD_PPG_RT_DATA = 0x1C;

    public static byte[] getPpgRt() {
        int len = 1;

        byte[] buf = new byte[8 + len];
        buf[0] = (byte) 0xAA;
        buf[1] = (byte) OXY_CMD_PPG_RT_DATA;
        buf[2] = (byte) ~OXY_CMD_PPG_RT_DATA;
        buf[5] = (byte) len;
        buf[6] = (byte) (len >> 8);
        buf[7] = (byte) 0X00;

        buf[8] = BleCRC.calCRC8(buf);
        return buf;

    }

https://github.com/viatomEcho/LepuBle/blob/46c1de44d2affddd5a0eb8719a7fbfdc8a3c761a/blepro/src/main/java/com/lepu/blepro/ble/cmd/OxyBleCmd.java#L25

This does correctly get a response back from the ring.

    @ExperimentalUnsignedTypes
    @Parcelize
    class RtWave(var byteArray: ByteArray) : Parcelable {
        val waveData: ByteArray
        val waveIntData: IntArray
        init {
            waveData = byteArray.copyOfRange(0, 5).toList().asSequence().map { (it.toInt() and 0x7f).toByte() }.toList().toByteArray()
            waveIntData =  byteArray.copyOfRange(0, 5).toList().asSequence().map { (it.toInt() and 0x7f)}.toList().toIntArray()
        }
    }

Defined:

CMD_RT_DATA = 27  # 0x1B rx len = <specified>, tx no data
CMD_PPG_RT_DATA = 28  # 0x1C
              self.send_func(o2pkt(CMD_PPG_RT_DATA, data="\x00\x01"))

...
        elif pkt.cmd == CMD_RT_DATA:
            print("Realtime | ", pkt.recv_data, "\n")
        elif pkt.cmd == CMD_PPG_RT_DATA:
            print("PPG | ", pkt.recv_data.hex(), "\n")
            print("PPG | ", pkt.recv_data, "\n")
z3ugma commented 1 year ago

Success!

image

I'll make a pull request based on CMD_RT_DATA, which pulls in the 125Hz PPG samples.

MackeyStingray commented 1 year ago

Thank you very much for this, @z3ugma ! Excellent work!

I got the original list of commands from capturing Bluetooth packets and giving them names which sounded descriptive. That LepuBle source is an awesome find.