Sunday, August 20, 2017

AFSK modulation experiments - success!

AFSK modulation experiments - success!


So, in the previous blog entry, I confirmed that I had come up with some numbers and timer values to make the frequencies I needed.  For posterity, I'll document them here.

DAC configration

I'm running my DAC with 24 data points.  I generated my sinewave table using the script that I blogged previously.  Based on the results from AFSK modulation experiments - part 4, I set the PPM on the Si5351b to 30, and the DAC_PCT to 35 to achieve about 3000hz deviation.  It looks as follows:

#define DAC_PCT 35          // Percentage of DAC output#define SINE_RES        24const uint16_t sinewave[SINE_RES] = {        (0 * DAC_PCT / 100) + 2048, (530 * DAC_PCT / 100) + 2048, (1023 * DAC_PCT / 100) + 2048,        (1448 * DAC_PCT / 100) + 2048, (1773 * DAC_PCT / 100) + 2048, (1978 * DAC_PCT / 100) + 2048,        (2047 * DAC_PCT / 100) + 2048, (1978 * DAC_PCT / 100) + 2048, (1773 * DAC_PCT / 100) + 2048,        (1448 * DAC_PCT / 100) + 2048, (1024 * DAC_PCT / 100) + 2048, (530 * DAC_PCT / 100) + 2048,        (0 * DAC_PCT / 100) + 2048, (-530 * DAC_PCT / 100) + 2048, (-1023 * DAC_PCT / 100) + 2048,        (-1448 * DAC_PCT / 100) + 2048, (-1773 * DAC_PCT / 100) + 2048, (-1978 * DAC_PCT / 100) + 2048,        (-2047 * DAC_PCT / 100) + 2048, (-1978 * DAC_PCT / 100) + 2048, (-1773 * DAC_PCT / 100) + 2048,        (-1448 * DAC_PCT / 100) + 2048, (-1024 * DAC_PCT / 100) + 2048, (-530 * DAC_PCT / 100) + 2048,};

Clock speed


I'm testing on the Nucleo-L152RE, which has a STM32L152RET6 processor.  My flght board uses an STM32L152CB, which is quite similar.  I'm running it at a clock speed of 32 MHZ, and the prescalars set such that the APB1 clock speed is also 32mhz.

RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;  RCC_OscInitStruct.HSIState = RCC_HSI_ON;  RCC_OscInitStruct.HSICalibrationValue = 16;  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL4; // 32mhz  RCC_OscInitStruct.PLL.PLLDIV = RCC_PLL_DIV2;  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)


Timer calculations


Based on the 32mhz clock, I calculated timers as follows.  The goal for the DAC timer was to be able to simply toggle the "counter" value back and forth, in order to change the tone between 1200hz and 2200hz.  You can calculate the Clock Ticks yourself by dividing:

Clock_Ticks_Hz = 32000000 / (DAC Datapoints * Prescaler * Counter)


Function Timer DAC sinewave
data points
Prescaler Counter Clock ticks (hz)
Baud Timer
1200hz
TIM2 n/a13,333 2 1200.03001
DAC timer
1200 hz
TIM6 24 101 11 1200.120012
DAC timer
2200 hz
TIM6 24101 6 2200.220022

  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 13333 - 1;
  htim2.Init.Period = 2 - 1; // 1200 hz
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;

  if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
  {
    Error_Handler(__FILE__, __LINE__);
  }

  htim6.Instance = TIM6;
  htim6.Init.Prescaler = 101 - 1;
  htim6.Init.Period = 11 - 1;
// 1200 hz

  htim6.Init.CounterMode = TIM_COUNTERMODE_UP;


  if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
  {
    Error_Handler(__FILE__, __LINE__);
  }
  // We turn on the ARPE register so that the counter is only updated AFTER a clean
  // count completes.  Otherwise, we run the risk of overcounting an interval.
  htim6.Instance->CR1  |= TIM_CR1_ARPE; // Turn on auto-preload register

The ARPE bit


Note that on the DAC timer (TIM6) we set the ARPE bit in the Control Register.   This causes any changes to the counter to take effect AFTER the next timer pop.  This assures that each timer cycle is complete, so that the DAC data points are properly spaced.


Switching between 1200hz and 2200hz

Because I'm using the DAC to make the sine wave, and just adjusting the TIM6 timer to change the frequency, I am able to greatly simplify the code that most APRS implementations use.  Rather than using a 512 element phase array, I'm simply using a 24 element sine array for the DAC.  I no longer need to track the phase position when switching.  Rather, I just adjust the timer counter.

The code is simple, but it confused me at first, because AX.25 is using NRZI encoding.  So, here's a bit about that, before showing the code.

From Wikipedia:
At the datalink level, AX.25 specifies HDLC (ISO 3309)[3] frames transmitted with NRZI encoding.   
In NRZI encoding, you're not sending the actual bits, but rather toggling whatever bit you are sending, based on the next data value.  I borrowed a few lines from the Tracksoar code, and modified them to my purposes:

if ((current_byte & 1) == 0) {
// Toggle tone (1200 <> 2200)
current_timer_counter ^= (COUNTER_1200 ^ COUNTER_2200); // Switch to the opposite counter value
__HAL_TIM_SET_AUTORELOAD(&htim6,current_timer_counter); // Set frequency
}

That's it.  No math about phase arrays and accmulators!  Yay!

Initial test failures


I only had 3 tiny bugs in my code which prevented APRS from working.


  1. I actually FORGOT the HAL_TIM_SET_AUTORELOAD command after setting the current_timer_counter.  lol.  
  2. The more subtle one was that I had misread the manual, and thought that the ARPE bit was set by default.  When I tested without it, there were hesitations in the middle of the transmission. It was obvious what was going on when I heard it, and simple to fix.
  3. I contrived the test by hard-coding GPS values into the strings that the aprssend() call uses.  I used the wrong number of digits in the Longitude, and so my Kenwood D72 caught my callsign, it wouldn't decode it until I fixed that coordinate.  Along the way, I noticed that my APRS comment was also too long.  I wanted it to read "Mr. Watson, come here, I want you." Unfortunately, that's too many characters.  So, I changed it to "Mr. Watson, Come here!..."

Successful test







Saturday, August 12, 2017

AFSK modulation experiments - part 4

AFSK modulation experiments - part 4


I left off the last time with some concerns that the deviation might be too wide on the VCXO, and also that when using the VCXO, the Si5351 was somewhat off frequency (about 5 khz).  I set out to work on both of those issues tonight.

I began by setting up my scope and service monitor so that I could measure the P2P value of the modulating sine wave, and the deviation of the resulting signal.


Here, we see a 1200hz sine wave the VCXO signal should be ve being generated by the DAC.  The P2P value is 0.980 volts.  This is from setting the sine wave at 30% amplitude.  We also see that the wave is centered at (2.18 + 1.20) / 2 = 1.69 volts. That's just fine, since it should be centered around 1.65 volts.



The resulting deviation for the wave above (on the 0-6 scale) is coming in at about 2.7 khz.  That's a touch low.

So, I ran the tests over and over gathering data. I also varied the PPM value to see the effect.


PPM value Sine wave % P2P voltage (volts) Deviation (hz)
30 50 1.52 3,500
30 30 0.982,700
30 40 1.243,300
40 40 1.243,700
40 30 0.983,200

Deviation Results

The PPM value definitely has an impact on the deviation.  Assuming that we would want the capability of 5 khz deviation for an FM signal, but then we would only want to modulate the signal to 3 khz, the ppm should be (5000 hz / 144.390) = 35 PPM.   In a previous post, I came up with the number 24, but that was when I was considering modulating to 3.2 khz, not 5 khz.

So, it appears that for flight use, I should be setting my PPM to 35 ppm, and setting my sinewave appropriately to get about a 3khz deviation.  That should be at about 30% or so.   I noticed a bit of variability when I was testing this evening, due to the fact that I was using breakout boards and jumpers all over.  I suspect this is a process I'll need to repeat when I have the production boards spun up.

Correcting for the incorrect frequency


I dabbled with the "Correction" call to fix the frequency of the VCXO this evening.

  MySi5351.set_correction(-12940);

In the end, the value above was sufficient to center up the frequency where I was testing, at 144.330 mhz.  According to the documentation, I can expect to do this with every board I build.

Conclusions



  1. I'll build my flight code with a PPM of 35.
  2. I'll build my flight code with a Calibration option.  When set, the tracker will generate a fixed tone (or maybe alternated between 1200 and 2200) so that I can confirm that the deviation is correct, and also so that I can diddle the "correction" value to assure the tracker is on frequency.





Friday, August 11, 2017

AFSK modulation experiments - part 3

AFSK modulation experiments - part 3


So, Part 2 of this little journey featured our hero barking up the wrong tree a bit.  I made two mistakes.
  1. I assumed that I had to modulate the VC pin on the Si5351b with values from -3v3 .. 3v3.  That is incorrect.  The proper values for the VC pin on the Si5351b are from 0 .. 3v3.  The "typical" value is listed as VDD / 2 (IE: 1.65 volts).  I presume (see below) that this means that any modulation should be centered at 1.65 volts.
  2. I assumed that the output of my signal generator was making an AC signal.  Of course, it wasn't, as I had things wired up.  It was a DC signal.
The mistakes above led to some seriously confusing results, and some pretty bad sounding signals.

Taking a step back


I decided to do some Science, and assure that I wasn't making any more bad assumptions.  I set up a test environment on my bench so that I could try moving the VC pin between 0 .. 3v3, and see the resulting impact on the signal being generated.

I wired up my Si5351b breakout board to my STM Nucleo-L152RE.  I ran the output into my spectrum analyzer so that I could get a good look at what was happening.  I wired the VC pin to a variable DC power supply so that I could vary it carefully, and see where the signal wound up.



Test 1: Baseline without VCXO


For the first test, I wanted to just set the frequency of the Si5351b to 144.330 without using the VCXO. This test was intended to see where the Si5351b was centered, without using the VC, so I had an idea of how well calibrated it was.

Here are the relevant lines which initialized the Si5351.  I am using the Etherkit Si5351 library.

    MySi5351.set_freq(14433000000ULL, SI5351_CLK0);
    MySi5351.output_enable(SI5351_CLK0, ENABLE);

This is the result.


My spectrum analyzer is reasonably well calibrated.  The center frequency of the Si5351b came in at 144.330004 mhz.  Dang.  Not bad at all!  My little breakout board actually uses a TCXO for the oscillator, and I think that helps with the accuracy.

Test 2: VC at 0 volts

  MySi5351.set_vcxo(86598000000ULL, 40);
  // Set CLK0 to be locked to VCXO
  MySi5351.set_ms_source(SI5351_CLK0, SI5351_PLLB);
  // Tune to 144.330 MHz center frequency
  MySi5351.set_freq_manual(14433000000ULL, 86598000000ULL, SI5351_CLK0);
  MySi5351.update_status();
  MySi5351.output_enable(SI5351_CLK0, ENABLE);



OK, that did just what one would expect.  It pulled the signal down to 144.323462 mhz.  That's 6.538 khz down.  Note, I set the PPM value to "40" in the initial tests, which is the value that was in the samples in the Etherkit Si5351 library I'm using.  Diddling with the PPM value will follow.

Test 3: VC at 3.3 volts


Great, OK, that set the top end of the swing.  It's at 144.332962.  I was a little surprised that it seemed a little asymmetric.   It's only 2.962 khz above the center (vs the 6,538 below we saw).

Test 4: VC at 1.65 volts


OK, this one, I would expect should show the signal centered up again.


Ruh Roh.  We should be at 144.330.  We're at 144.327212.   That's about 5khz off where I expected. Not cool.  If I split the difference between the low and high values, I should have been at 144.328212, which would still have been 1.788 khz off.

Test 5: What voltage do I need for Center frequency?




OK, I was able to get it centered up.  Rather than being at 1.65 volts, I had to apply 2.43 volts. Curious.

Test 6: How does modulation look at 1.65 volts with the DAC?


So, my DAC was all set up to make about 1 volt P2P, centered at 1.65 volts.  I put that 1200 hz modulation into the VC pin to see what it would look like,


Sure enough, it looks symmetric around 133.327 mhz, the same as when the VC pin is just held at 1.65 volts.



Other thoughts on the frequency error

In a later test, I had the VCXO set at 146.000 mhz.  When set up that way, the frequency was dead nuts on at 1.65 volts.  Modulating the frequency looked just fine, too.

I suspect that the frequency error has something to do with an error introduced by the multisynth multipliers and dividers.  I'm not too concerned, as I may be able to address it either by using the calibration subroutines, or different multipliers.

Deviation and the PPM value

OK, so I had set the PPM value in all of these tests to 40.  At 144.330 mhz, that should make for a deviation of  3.608 khz.  My observed deviation was 144.332962 - 144.323462 = 9.500 khz,  

Hmm...   Is that how you measure deviation?  If so, mine's WAY too big.

Test 7:  Different PPM values



PPM value 0 volt reading 3.3 volt reading Delta
30 144.325164 mhz 144.332289 mhz 7,127 hz
40 144.323462 mhz 144.332962 mhz 9,500 hz

I also tried a test with 20 PPM (which isn't supported according to the documentation), and it seemed to show the same readings as 30 PPM.

If I understand "deviation" in this context, I think these numbers are too big.  I need to understand this better.

Next steps


Strictly speaking, the frequency being off isn't too much of a problem.  The Si5351 has a calibration routine which I can use to center this up.  It's just odd that the center frequency is different when using the VCXO.  I'm a little more concerned about the deviation right now.  The lowest deviation setting still seems to be too wide, unless I'm misunderstanding what to expect with regard to this kind of experiment.

Things to do:
  1. Call the calibration routine to see if I can get it centered up.
  2. Experiment with the amplitude of the VC signal to see the effect on the signal, and received modulation.
  3. See if different multipliers work to improve the frequency accuracy of the VCXO.  I used 6 for these tests.   I could try 4 or 8




Generating a sinewave table for my DAC

Generating a sinewave table for my DAC


Unlike other AFSK implementations, I don't actually need a sinewave table with a lot (like 512) of values in it.  In fact, I'll be using just 24 values in my sinewave table for my DAC.

I decided to write a program to generate my table.  While I was at it, I decided that I would have it build in a scaling factor as a "#define".  By changing that one #define, I can change the amplitude of the sinewave with a recompile, rather than having to re-generate a whole new table.

Sinewave.pl

#!/usr/bin/perl
# Usage: sinewave.pl samples
# Generates a sinewave table containing the specified number of samples.
# For example, "sinewave.pl 36" yields:
#       #DEFINE DAC_PCT 100
#       #DEFINE SINE_RES        36
#       const uint16_t sinewave[SIN_RES] = { .... }
# The DAC_PCTxx define is a percentage multipler against all of the values in the table.  Useful if your DAC output is too high.
# By changing this one DEFINE in your code, you can scale the sinewave table at compile time.  Note, that the DAC values will remain
# centered on the sine(0) value, and will scale "inward" toward that number.  For example, on a 12 bit DAC, sin(0) is going to be at 2048.
# Even if you scale up and down, the sine wave will be centered around 2048.
#
# The output of this program is actually a C program which an be compiled to confirm the table is built properly.  When satisfied,
# you can just copy/paste the sinewave table and the few necessary defines into your own program.
use strict;
my $DACBITS = 12;                       # Number of bits the DAC (or pwm) is using
my $COLUMNS_PER_LINE = 3;               # Number of values per row
if ($#ARGV != 0) {
        print STDERR "Usage: $0 samples\n";
        exit 4;
}
my $center_value = 2 ** $DACBITS / 2;   # The peak 2 peak of the sine wave is twice $center_value, so we divide by two.
my $PI = 3.1415926;
my $samples = $ARGV[0];
my $degrees = 0;
my $column;
my $value;
my $row;
print("\#include \"stdint.h\"\n");
print("\#include <stdio.h>\n");
print("main()\n");
print("{\n");

print("\#define DAC_PCT 100             // Percentage of DAC output\n");
print("\#define SINE_RES        $samples\n");
print("const uint16_t sinewave\[SINE_RES\] = {\n");
for ($row = 0; $row < ($samples / $COLUMNS_PER_LINE); $row++) {
        print("\t");
        for ($column = 0; (($degrees < 360) &&  ($column < $COLUMNS_PER_LINE)); $degrees += 360/$samples) {
                $value = int($center_value * sin($degrees/180.0 * $PI));                # Perl sin() is in Radians.  Convert.
                print("(${value} * DAC_PCT / 100) + $center_value, ");
                $column++;
        }
        print("\n");
}
print("};\n");
print("int      i;\n\n");
print("for (i = 0; i < SINE_RES; i++) {\n");
print("\t");
print("printf(\"\%d\\n\", sinewave[i]);");
print("\n"); print("\t}\n"); print("}\n");

What it does

Rather than just creating the sinewave table, it actually generates a short C program which can be compiled in Unix to confirm that the table is properly formed, and that the values look correct.


Sample run for 24 data points

mqh1@debian:~/wisp2$ ./sinewave.pl 24
#include "stdint.h"
#include <stdio.h>
main()
{
#define DAC_PCT 100             // Percentage of DAC output
#define SINE_RES        24
const uint16_t sinewave[SINE_RES] = {
        (0 * DAC_PCT / 100) + 2048, (530 * DAC_PCT / 100) + 2048, (1023 * DAC_PCT / 100) + 2048,
        (1448 * DAC_PCT / 100) + 2048, (1773 * DAC_PCT / 100) + 2048, (1978 * DAC_PCT / 100) + 2048,
        (2047 * DAC_PCT / 100) + 2048, (1978 * DAC_PCT / 100) + 2048, (1773 * DAC_PCT / 100) + 2048,
        (1448 * DAC_PCT / 100) + 2048, (1024 * DAC_PCT / 100) + 2048, (530 * DAC_PCT / 100) + 2048,
        (0 * DAC_PCT / 100) + 2048, (-530 * DAC_PCT / 100) + 2048, (-1023 * DAC_PCT / 100) + 2048,
        (-1448 * DAC_PCT / 100) + 2048, (-1773 * DAC_PCT / 100) + 2048, (-1978 * DAC_PCT / 100) + 2048,
        (-2047 * DAC_PCT / 100) + 2048, (-1978 * DAC_PCT / 100) + 2048, (-1773 * DAC_PCT / 100) + 2048,
        (-1448 * DAC_PCT / 100) + 2048, (-1024 * DAC_PCT / 100) + 2048, (-530 * DAC_PCT / 100) + 2048,
};
int     i;
for (i = 0; i < SINE_RES; i++) {
        printf("%d\n", sinewave[i]);
        }
}
mqh1@debian:~/wisp2$

Compile and run the output


mqh1@debian:~/wisp2$ ./sinewave.pl 24 > t.c
mqh1@debian:~/wisp2$ cc t.c
mqh1@debian:~/wisp2$ ./a.out
2048
2578
3071
3496
3821
4026
4095
4026
3821
3496
3072
2578
2048
1518
1025
600
275
70
1
70
275
600
1024
1518
mqh1@debian:~/wisp2$

In the output above, you can see that it's got a sinewave centered at 2048, with values ranging from (almost) 0 to 4095.

Re-scaling the sine wave

To change the scale out the sinewave, I simply modify the #define

#define DAC_PCT 50              // Percentage of DAC output

This will set the DAC amplitude at 50%

mqh1@debian:~/wisp2$ vi t.c
mqh1@debian:~/wisp2$ cc t.c
mqh1@debian:~/wisp2$ ./a.out
2048
2313
2559
2772
2934
3037
3071
3037
2934
2772
2560
2313
2048
1783
1537
1324
1162
1059
1025
1059
1162
1324
1536
1783
Note that the center is still around 2048, but the amplitude only goes up and down by about 1024.



Wednesday, August 2, 2017

Plotting a maidenhead grid square in aprsisce

Plotting a maidenhead grid square in aprsisce


The WSPR protocol uses Maidenhead grid squares to plot location.  The granularity of a 6-character grid square is approximate 3x4 miles, which is fine for WSPR balloons in the air.  Unfortunately, if a payload crashes on the ground, it's not as useful.  Twice, now, I've tried to locate a WSPR payload on the ground by driving around and trying to hear the signal.  I find it useful to have Aprsisce running in my car, and plotting the grid square on the map, so that I know when I'm reaching the borders of the search area.  Unfortunately, I can never remember how I did it.  This blog entry is a "note to self" for the next time this comes up.

Convert grid square to KML

Visit this site, and enter the 6 character grid square.  It outputs a .kml file.

Convert the KML file to a GPX file

Visit this site and conver the kml to gpx.

Load the GPX file as an overlay in aprsisce

Inside aprsisce:  Configure / Overlay / Add GPX file...

Set the opacity and color so that it shows nicely on the map.


AFSK modulation experiments - Part 2

AFSK modulation experiments - Part 2

Prelude

The experiments below were an initial attempt at modulation.  After posting this blog, I discovered I made an error.  the VC pin should take values between 0 .. 3v3.  I was attempting to modulate with an AC voltage, which was broken in several different ways.  Live and learn.

The erroneous post follows, for posterity.  However, corrections will be evident in blog posts made after this one.

Read below at your own peril.  I made lots of mistakes!!!

Test code

So, I'm using the Etherkit Library to generate my signals.  I used a little test code this evening to configure the VCXO and generate a signal at 144.330.  I then attached my signal generator to the VC pin and adjusted the frequency and amplitude to see if I could modulate my signal.  It worked!

The code is mostly just the example code available on his site.
// ASTXR-12-26.000MHz-512545 TCXO is 26 MHZ.  We use 0PF, since it's a TCXO and not a crystal.
MySi5351.init(SI5351_CRYSTAL_LOAD_0PF, 26000000UL, 0UL);
// power down the unused clocks
MySi5351.set_clock_pwr(SI5351_CLK1, DISABLE);
MySi5351.set_clock_pwr(SI5351_CLK2, DISABLE);
// Turn off transmit on unused clocks
XMITOFF(SI5351_CLK1);
XMITOFF(SI5351_CLK2);
MySi5351.set_clock_pwr(SI5351_CLK0, ENABLE);
XMITOFF(SI5351_CLK0);
MySi5351.drive_strength(SI5351_CLK0,SI5351_DRIVE_8MA); // Max power
MySi5351.set_vcxo(86598000000ULL, 40);
// Set CLK0 to be locked to VCXO
MySi5351.set_ms_source(SI5351_CLK0, SI5351_PLLB);
// Tune to 144.390 MHz center frequency
MySi5351.set_freq_manual(14433000000ULL, 86598000000ULL, SI5351_CLK0);
MySi5351.update_status();
MySi5351.output_enable(SI5351_CLK0, ENABLE);

Note, I have not yet fiddled with the PPM parameter as discussed in the previous blog.  I left it at "40" from the example.

Amplitude fiddling

I noticed that the amplitude of the signal generator output changed significantly when plugged into the VC pin.  I dialed the amplitude up and down until I reached a reasonable level, without distortion. I noted that under load, the P2P amplitude was about 1.16v.  When attached to the VC pin, the amplitude went down to about 0.6v p2p.


DAC fiddling

Knowing the amplitude, I was curious what output level the DAC would generate with the sine wave tables I was using for maximum amplitude.  Not surprisingly, it was about 3v p2p.  Not a shocker, since the Si5351 is operating at 3v3.  I needed to lower the amplitude of the DAC output to match what I was getting from the signal generator (1.16v).

The RC filter, tested in the Part 1 blog for this project, cleaned up the signal significantly, and also reduces the amplitude.  That's a start.

DC Blocking Cap


The signal generator makes an AC signal, swinging positive and negative.  The DAC makes a sine wave that is strictly positive.  I introduced a DC blocking cap into the circuit to convert it to AC, and clean it up a bit.

Test circuit



This circuit was sufficient to convert the DAC output to filtered AC.  The P2P voltage was about 1.5 volts, but it was sufficient for testing.  Sure 'nuff, it worked.  Putting the "AC Signal Out" from this filter into the VC pin on the Si5351b modulated the signal, and the tone created on the Nucleo DAC was readable on an FM receiver.

Progress!

Afterword


So, upon further examination, I discovered that I was in error.  The VC pin on the Si5351 requires 0 .. 3v3, all positive.  The experiments I was doing were generating an AC signal, but the modulation was faulty.  I'll have more on this in a future blog post.