Tinkering with a Kospet ROCK

Posted on
  • Hi there !
    I recently got a Kospet ROCK (C16) smartwatch, that I almost instantly turned into a near-useless Espruino-powered tinkering playground thanks to the instructions shared by @fanoush and @jeffmer.

    I don't have a lot of experience in embedded stuff apart from playing a bit with adafruit's circuitpython, but I'm looking forward to make my watch vaguely useful again with some code of my own.

    I don't have a precise idea yet of what I want to do with it, apart from restoring its time and fitness abilities.

    Code will be here :
    https://gitlab.com/ddelemeny/watchapps

    Any other projects on this watch I should know about ?

  • A few thoughts about the development on this watch:

    • I don't really use the web IDE except for the REPL and to deploy files to storage. I'd prefer to use something more streamlined than a platform-specific IDE. Any advice ?
    • I tend to use a lot of closures in my code, and I'm wondering if the overhead of that coding style is going to be significant enough to be a problem.
    • A lot of the device-specific stuff feels like copy-pasting magic runes from a more advanced wizard.
  • I don't have a precise idea yet of what I want to do with it, apart from restoring its time and fitness abilities.

    Not sure about the fitness part, not even sure @jeffmer got the heartrate sensor working.

    Any other projects on this watch I should know about ?

    There is also eucWatch made by @enaon
    https://github.com/enaon/eucWatch/tree/main
    https://enaon.github.io/eucWatch/

    I tend to use a lot of closures in my code, and I'm wondering if the overhead of that coding style is going to be significant enough to be a problem.

    closures are not an issue but low memory in general is

  • There is also eucWatch made by @enaon

    Thanks for the pointer !

    Not sure about the fitness part, not even sure @jeffmer got the heartrate sensor working.

    I'm currently looking into the existing HRS code, any particular reason it would be difficult to implement ?

    closures are not an issue but low memory in general is

    What are the available strategies to profile memory usage on Espruino ?

  • I'm currently looking into the existing HRS code, any particular reason it would be difficult to implement ?

    Okay, got it... the datasheet for the HRS3603 isn't exactly dev friendly... and then he disappeared into the rabbit hole

    Edit : https://github.com/theocoutu/HX3600-Arduino-Library

  • I managed to translate part of the driver mentioned above to js and get some readings. BPM values are all over the place, but that's a start...

  • Good find, I think we did not have any source and the datasheet did not say much.
    When searching github now https://github.com/search?q=hrs3603.c&type=code there is something added about 1 year ago.

    Often the glue source comes with some linkable library with the HR/BP/SPO2 algorithm, without that you only get raw PPG data.

    Also the trick can be with tuning it on the fly so that the PPG data is good quality see this example for VC31 in bangle https://github.com/espruino/Espruino/blob/master/libs/misc/hrm_vc31.c#L268

  • Thanks!

    Edit: deleted wild guesswork, misread the datasheet. I don't know what I'm doing.

    On the algorithm part, I didn't find the code using that driver (it seems to be dead code, replaced by a driver for the HX3605), but I expect to be able to use what jeffmer has for processing the signal.

  • I guess I'm an idiot. Readings were all over the place because I was reading noise. Led doesn't seem to turn on.

  • Here's my "driver" at this point, if anyone wants to give a shot at understanding what's missing.

    // Driver for HX3603, see https://github.com/moyitai/5.26-sdk-1.0.2--integrate/blob/master/apps/common/device/hr_sensor/hrs3603.c
    
    var dev = wOS.i2c.bind(0x44); // device I2C address
    
    functionHX3603 (){
    var conf_hrs = {
      // Clock
      SAMPLE_RATE : 25, // Hz
      PRF_CLK_NUM : 32000/SAMPLE_RATE,
      // PS1 PRF [0-255]
      PS1_INTERVAL_I2C : 64, // NOTE : 3600 datasheet reco
    
      // Phase Enable
      HRS_ENABLE : 1,
      PS0_ENABLE : 0,
      PS1_ENABLE : 1,
      TS_ENABLE : 0,
    
      // Oversampling rate 0:128 1:256 2:512 3:1024
      HRS_PS0_TS_OSR : 3,
      PS1_OSR : 3,
    
      // LED & pulse delay
      HRS_LED : 1,
      HRS_CKAFE : 1,
      HRS_CKAFE_CYCLE : 50,
    
      PS1_LED : 1,
      PS1_CKAFE : 1,
      PS1_CKAFE_CYCLE : 50,
    
      // LED Driver current
      HRS_LEDDR_MSB : 3,
      PS1_LEDDR_MSB : 7,
      LEDDR_LSB : 7,
    
      // LES SEL ?
      LEDSEL_HRS : 1,
      LEDSEL_PS1 : 2,
      FORCE_LEDSEL : 0,
    
      RESET_CYCLE : 5,
    
      // DC signal canceling
      DCCANCEL_HRS_IDAC : 0,
      DCCANCEL_PS1_IDAC : 0,
    
      // EXT PD (power delivery) // Undocumented
      EXTPD_SEL : 1,
      EXTPD_PS1 : 1,
      EXTPD_HRS : 1,
    
      // TIA 1 | Integrator 0
      TIA_PS1 : 1,
      TIA_HRS : 1,
      // TIA feedback resistor
      RFSEL_HRS : 6,
      RFSEL_PS1 : 1,
      // Integrator Capacitor ?
      HRS_PS0_TS_INT_CAP : 15,
      PS1_INT_CAP : 15,
    
      // Clock polatiry ?
      ADC_DATA_CLK_POL : 3,
    };
    var mode_hrs = 1;
    var hrs = {
      dev:dev,
      disable: ()=>{
        dev.writeByte(0x01, 0x01); 
        dev.writeByte(0x02, 0x01); 
        delay(50);
        dev.writeByte(0x1a, 0x13); 
      },
      enable: ()=>{
        var c = conf_hrs;
        dev.writeByte(0x1e, 0x00); 
        // Clock config
        dev.writeByte(0x10, c.PRF_CLK_NUM & 0xff); 
        dev.writeByte(0x11, (c.PRF_CLK_NUM & 0x0f00)>>8); 
    
        // Phase enable and OSR
        dev.writeByte(0x01, c.TS_ENABLE<<4 | c.PS0_ENABLE <<3 | c.HRS_ENABLE<<2 | c.HRS_PS0_TS_OSR);
        dev.writeByte(0x02, c.PS1_ENABLE<<2 | c.PS1_OSR);
        
        // LED enable and AFE clock enable
        dev.writeByte(0x03, c.HRS_LED<<3 | c.PS1_LED<<2 | c.HRS_CKAFE<<1 | c.PS1_CKAFE); // 00001111 // ?:0000|HRS_LED:1|PS1_LED:1|HRS_CKAFE:1|PS1_CKAFE:1
        dev.writeByte(0x08, c.HRS_CKAFE_CYCLE); // hrs ledontime, 0~255,  led on time = 4*hrsckafe_cycle*0.382us
        dev.writeByte(0x09, c.PS1_CKAFE_CYCLE); // ps1 ledontime, 0~255,  led on time = 4*ps1_ckafe_cycle*0.382us
    
        // clock reset config 
        dev.writeByte(0x04, c.RESET_CYCLE); // 00000101 // 3 bit RESET CYCLE sel //  (2^(reset_cycle+1))-1*/
    
        // PRF
        dev.writeByte(0x1c, c.PS1_INTERVAL_I2C); // PS1 PRF 0-255 - 25 mean 1s
        dev.writeByte(0x05, c.DCCANCEL_PS1_IDAC); // offset DCCANCEL PS1 
        dev.writeByte(0x06, c.DCCANCEL_HRS_IDAC); // offset DCCANCEL HRS/TS/PS0
    
        dev.writeByte(0x07, c.LEDSEL_PS1<<4 | c.FORCE_LEDSEL<<3 | LEDSEL_HRS); // 00100010 // ?:0|LEDSEL_PS1:010|FORCE_LEDSEL:0|LEDSEL_HRS:001 
        dev.writeByte(0x0a, c.PS1_INT_CAP<<4 | c.HRS_PS0_TS_INT_CAP); // 11111111 // INTCAPSEL Integrator gain ?
    
        dev.writeByte(0x0b, c.PS1_LEDDR_MSB<<4 | c.HRS_LEDDR_MSB); // 01110011 // PS1:0111|HRS:0011 LEDDR [0-7]
    
        dev.writeByte(0xc4, c.EXTPD_SEL<<7 | (c.LEDDR_LSB&0x07)<<4 | 0x0f); // 11111111 // extpd_sel | leddr_lsb | 0x0f
    
        dev.writeByte(0x0d, c.RFSEL_HRS <<4 | c.RFSEL_PS1); // 01100001 // rfsel_hrs | rfsel_ps1
        dev.writeByte(0x0c, c.RFSEL_PS1); // 00000001 // rfsel_ps1 feedback resistor ?
        dev.writeByte(0x0f, 0x00); // 
        dev.writeByte(0x0e, c.EXTPD_PS1<<3 | c.EXTPD_HRS<<2 | c.TIA_PS1<<1 | c.TIA_HRS); // 00001111 extpd_ps1:1 | extpd_hrs:1 | tia_ps1:1 | tia_hrs:1
    
        dev.writeByte(0x1b, 0x7f); //
        dev.writeByte(0xc2, 0x10); // Self test
        dev.writeByte(0xc3, 0xff); //
        dev.writeByte(0x18, c.ADC_DATA_CLK_POL); // adc_data_clk_pol
    
        dev.writeByte(0x1a, 0x12); // 
        dev.writeByte(0x13, 0x02); // enable mode (ps1|hrs)
        dev.writeByte(0x12, 0x50); //
        dev.writeByte(0x20, 0x20); // close fifo int
      },
      read_hrs:()=>{
        var data = dev.readBytes(0xa0,12);
        var p0 = (data[0]) | (data[1]<<8) | (data[2]<<16);
        var p1 = (data[3]) | (data[4]<<8) | (data[5]<<16);
        return { als:[p0, p1], hrs: (p0>p1)?p0-p1:0};
      }
    };
    return hrs;
    }
    

    And here's the module I use it with

    var correlator = function(cspan) {
      var CMIN = 7; var CMAX = 37; var NSLOT = 128;
      var buffer = new Array(NSLOT); var next=0;
      cspan = cspan || 1;
      return {
        put: (v)=>{ buffer[next] = v; next = (next+1)%NSLOT; },
        get bpm(){
          var minCorr=0x7FFFFFFF;
          var span=0;
          for (var c=CMIN; c<CMAX; c++) {
            var s = 0; var a = (next-c*cspan) % NSLOT; var b = next;
            for (var i=0; i<(NSLOT-CMAX);i++){
              var d = buffer[b]-buffer[a];
              b = (b+1)%NSLOT; a = (a+1)%NSLOT;
              s += d*d;
            }
            if (s<minCorr) {minCorr = s; span = c;}
          }
          return span == 0 ? 0:(60000/(span*40));
        }
      };
    };
    
    function hrm(){
      var corr = correlator();
      var iv;
      var hrm = {
        read:()=>{
          var v = wOS.hrs.read_hrs();
          corr.put(v.hrs);
        },
        start: (record)=>{
          if (iv) return;
          wOS.hrs.enable();
          iv = setInterval(hrm.read, 40);
        },
        stop: ()=>{
          if (!iv) return;
          iv = clearInterval(iv);
          wOS.hrs.disable();
        },
        get bpm() { return corr.bpm(); },
      };
      return hrm;
    }
    
  • This is the datasheet for what seems to be an earlier iteration of the same chip architecture. Register addresses are not accurate and some are missing, but it looks overall closer to the 3603 than the 3605, 3300 or 3313 and provides a bit more info.

    Edit : scratch that, it's basically the same as the 3313.

    http://www.synercontech.com/Public/uploads/file/2019_10/20191020152311_81180.pdf

  • Le led driver current control implementation is described like this in the 3603 datasheet :

    LED current range 3.2 to 204.8 mA
    LED current resolution 6 Bits
    ...
    The device has one internal current DAC 3bits output current control.
    The output current control though register 0x0Bbit2:bit0for phase1
    to phase3.

    And in the driver :

    // LED Driver current
    HRS_LEDDR_MSB : 3,
    PS1_LEDDR_MSB : 7,
    LEDDR_LSB : 7,
    ...
    dev.writeByte(0x0b, c.PS1_LEDDR_MSB<<4 | c.HRS_LEDDR_MSB);
    dev.writeByte(0xc4, c.EXTPD_SEL<<7 | (c.LEDDR_LSB&0x07)<<4 | 0x0f);
    

    Look vaguely consistent. Shame that the 0xC4 register isn't documented (among others).

  • One weird thing (to me, maybe it's normal) is that the I2C device needs to be 'primed' by a first read (write?) to its registers to have them initialized. The first read seems to be garbage.

    Here's attached a dump of the registers after priming, with annotations.


    1 Attachment

  • A few updates :

    • Added some device drivers (accel, touch, buzzer)
    • Added a context manager and a few utils to Graphics
    • Started implementing an execution manager module, the objective is to have transition management between apps
    • Added two simple clock apps (one is a reimplementation of slopeclock, not yet animated)

    I'm not trying to be compatible with Bangle.js, maybe I'll end up implementing something very similar, maybe not.

    I'd like to tinker a bit with low-level graphics eventually in order to implement some efficient masking, but I'm kind of afraid to brick my device in the process.


    1 Attachment

    • 20230906_095310.jpg
  • Nice :-)

    but I'm kind of afraid to brick my device in the process.

    Use watchdog, ping it only when button is not held. Something like this https://github.com/fanoush/ds-d6/blob/master/espruino/DFU/Magic3/code.js#L1C1-L6C1
    will reboot if something freezes the interpreter event loop or you hold the button. With Rock you can even use the second button for that (BTN2)

    If you flashed my bootloader then it is standard Espruino bootloader so it should enter DFU mode when you hold button at reboot time for less than 3 seconds or it enters Espruino but avoids running any js code at startup if you hold it longer (test it now :-), if you put watchdog pinging on BTN2 then you could decide to just reboot (by holding BTN2) or also enter DFU or skip code at startup by holding both.

    You could also enable watchdog directly inside C startup code if you modify Espruino FW and are afraid of freezing at FW initialization before js code is executed.

    Also if you don't use or trust watchdog then keep battery level low and/or enable something that drains battery fast to let it die sooner (backlight, or maybe Serial port is more harmless - it should die in a day or two from full battery) and you can then hold BTN1 before you attach charger.

  • I do use the watchdog already, a lot of my early code comes from your demo and jeffmer's repo.

    However what I'd like to do is to create a few graphics routines in C to avoid the overhead from the interpreter. At the moment I'm drawing overlays upon overlays to achieve masking, which is pretty suboptimal.

    Gotta put my head into custom builds but that's a lot of foreign stuff to take in :).

    Edit : That jswrap_graphics.c file is brutal.

  • If we could have Kospet Rock with transflective display 🤤

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Tinkering with a Kospet ROCK

Posted by Avatar for Kaoron @Kaoron

Actions