RC Modelling

In my youth, I started to build classic flying models. First a Dandy, then the Amigo and finally motor flight by Taxi.
After my schooldays, I spent many years and then started again with a hotliner, but the charm was no longer there … something new had to come from.
For years I had dreamed of model helicopters in my youth and now they were affordable for me.
So sold the hotliner and started classic with the Hubis: coaxial with Ikarus Piccolo, then the ECO 8, a Zoom 400 and a Logo 10.

When I’m doing sports ๐Ÿ™‚ I would like to throw my SAL in the air

But actually I always stayed with the Mikado Helis and fly a Logo 400.

In the meantime, I have replaced my good old Multiplex MC 3030 with a Multiplex Cockpit SX 9 system with telemetry. Great part!
The possibilities of telemetry have really hooked me up and so I immediately started to deal with the topic.

First of all I equipped my “load carrier”, a DJI 550 hexacopter with an AnySense telemetry module. Cool thing, but still is the V1.1 version and therefore can not send alerts to my multiplex cockpit SX 9 (details can be read in the link of the next paragraph). Since I have something to tinker ๐Ÿ˜‰

 

The Multiplex Sensor Bus (MSB)

(Really good description by cyblord in Microcontroller.net)

I then started to build my own MSB Sensor Emulator, MSB Sensor or MSB Sensor Logger based on a Teensy 3.2 (Arduino compatible all in one board). The result is …

The FrSKY MSB protocol converter

This means that almost any number of FrSKY sensors can be connected to my protocol converter via the FrSKY bus, which in turn is connected as a sensor to a multiplex receiver with MSB. The protocol converter polls – periodically polls the FrSKY sensors on the bus – and at the same time it emulates all the corresponding sensors on the MSB side and sends the corresponding information to the receiver.


Here is a picture of the components used without cabling.

If someone wants to build something like this here are some lines of code of the corresponding Arduino development environment.

/*
* FryLink Telemetry Converter Includes
*
* from Burkhard Vogt, Muenster, Westfalia, Germany
*
* These are the defines for FrSky S.Port
*
*/

#define FrSkyPIdAnz 26

const byte FrSkyPId[] =
{
0x00, // Physical ID 1 – Vario2 (altimeter high precision)
0xA1, // Physical ID 2 – FLVSS Lipo sensor (can be sent with one or two cell voltages)
0x22, // Physical ID 3 – FAS-40S current sensor
0x83, // Physical ID 4 – GPS / altimeter (normal precision)
0xE4, // Physical ID 5 – RPM
0x45, // Physical ID 6 – SP2UART(Host)
0xC6, // Physical ID 7 – SPUART(Remote)
0x67, // Physical ID 8 –
0x48, // Physical ID 9 –
0xE9, // Physical ID 10 –
0x6A, // Physical ID 11 –
0xCB, // Physical ID 12 –
0xAC, // Physical ID 13 –
0x0D, // Physical ID 14 –
0x8E, // Physical ID 15 –
0x2F, // Physical ID 16 –
0xD0, // Physical ID 17 –
0x71, // Physical ID 18 –
0xF2, // Physical ID 19 –
0x53, // Physical ID 20 –
0x34, // Physical ID 21 –
0x95, // Physical ID 22 –
0x16, // Physical ID 23 –
0xB7, // Physical ID 24 – Receiver quality ?
0x98, // Physical ID 25 – Fuel ?
0x39, // Physical ID 26 –
0xBA, // Physical ID 27 – Timer ?
0x1B // Physical ID 28 –
};

/*
* FryLink Telemetry Converter
*
* from Burkhard Vogt, Munster, Westfalia, Germany
*
* Idea and sources from Jochen Tuchbreiter
*
* Connections:
* Vin – Input (Bus) power by RX of MPX
* Gnd
* A1 – Signal wire MLINK
* A3 – Signal LED (green)
* A6 – Setup LED (yellow)
* A10 – Signal wire FrSky
* A12 – Signal LED (red)
*/

#include <EEPROM.h>

struct Sensor
{
unsigned short uTypID;
short iMin;
short iMax;
byte bAlert;
short iValue;
byte bClass;
byte bBuzz;
unsigned long ulMsec;
}
SensDef[16];

// #define DEBUG
// #define DEBUG_SETUP
// #define DEBUG_MLINK
// #define DEBUG_FRSKY

// Serial interfaces used

#define DebugSerial Serial
#define MLinkSerial Serial1
#define FrSkySerial Serial2

#define MLinkSerial_C1 UART0_C1
#define MLinkSerial_C3 UART0_C3
#define FrSkySerial_C1 UART1_C1
#define FrSkySerial_C3 UART1_C3

// Some defines for the serial communication

#define FRSKY_POLLING 10 // Time between two polling requests
#define MLINK_DELAY 3 // Time between request and answer

// Some global variables for the LEDs

const byte MLinkLED = 3; // The green MLink LED
const byte SetupLED = 6; // The yellow Setup LED
const byte FrSkyLED = 12; // The red FrSky LED

// Some global variables for the setup

#define SETUP_BEGIN 0xBA
#define SETUP_READ 0xBB
#define SETUP_WRITE 0xBC
#define SETUP_END 0xBE
#define SETUP_ESC 0xBF

// Write sensor definitions

void WriteSetting ( byte bIdx )
{
#ifdef DEBUG_SETUP
DebugSerial.print ( “Write setting to EEPROM for ” );
DebugSerial.println ( bIdx );
#endif

byte bI, *bBytes = (byte *) &SensDef[bIdx];

for ( bI=0; bI<8; bI++ )
{
#ifdef DEBUG_SETUP
DebugSerial.print ( bBytes[bI], HEX );
DebugSerial.print ( ” ” );
#endif

if ( EEPROM[bIdx*8+bI] != bBytes[bI] ) EEPROM[bIdx*8+bI] = bBytes[bI];
}

#ifdef DEBUG_SETUP
DebugSerial.println ();
#endif
}

// Sets the right class for given TypID

void SetSensClass ( byte bIdx )
{
switch ( SensDef[bIdx].uTypID )
{
case 0x0200:

SensDef[bIdx].bClass = 2; // Current

break;

default:

SensDef[bIdx].bClass = 1; // Voltage
}
return;
}

// Read sensor definitions

void ReadSettings ()
{
byte bI, bIdx = 0;

#ifdef DEBUG_SETUP
DebugSerial.println ( “Read setting from EEPROM” );
#endif

for ( bI=0; bI<16; bI++)
{
byte bK, *bBytes;

bBytes = (byte *) &SensDef[bI];

#ifdef DEBUG_SETUP
DebugSerial.print ( bIdx );
DebugSerial.print ( ” – ” );
#endif

for ( bK=0; bK<8; bK++ )
{
bBytes[bK] = EEPROM[bIdx++];

#ifdef DEBUG_SETUP
DebugSerial.print ( bBytes[bK], HEX );
DebugSerial.print ( ” ” );
#endif

SetSensClass ( bI );

SensDef[bI].iValue = 0x8000;
SensDef[bI].bBuzz = 0;
SensDef[bI].ulMsec = 0;
}
#ifdef DEBUG_SETUP
DebugSerial.println ();
#endif
}
}

// Startup function to initialize serial communication,
// global settings, some led blinking and so on

void setup()
{
#ifdef DEBUG
while ( !DebugSerial ) { ; }
DebugSerial.println ( “Program and funktion setup start” );
#endif

// Setup the pin states

pinMode ( MLinkLED, OUTPUT );
pinMode ( FrSkyLED, OUTPUT );
pinMode ( SetupLED, OUTPUT );

// Setup the serial communication

DebugSerial.begin ( 9600 );
MLinkSerial.begin ( 38400 );
FrSkySerial.begin ( 57600, SERIAL_8N1_RXINV_TXINV );

MLinkSerial_C1 |= 0x20 + 0x80; // Single wire mode

#ifdef DEBUG_SETUP
DebugSerial.println ( “Set MLink single wire mode” );
#endif

FrSkySerial_C1 |= 0x20 + 0x80; // Single wire mode

#ifdef DEBUG_SETUP
DebugSerial.println ( “Set FrSky single wire mode” );
DebugSerial.println ( “Set FrSky TX/RX invert” );
#endif

MLinkSerial.clear();
FrSkySerial.clear();

byte bI;

ReadSettings ();

byte bCmd = 0;

for ( bI=0; bI<12; bI++)
{
byte bLed = bI % 3;

digitalWrite ( MLinkLED, bLed == 0 ? HIGH : LOW );
digitalWrite ( SetupLED, bLed == 1 ? HIGH : LOW );
digitalWrite ( FrSkyLED, bLed == 2 ? HIGH : LOW );

bCmd = MLinkSerial.read();

if ( bCmd == SETUP_BEGIN ) break;

delay ( 500 );
}

digitalWrite ( FrSkyLED, LOW );

#ifdef DEBUG_SETUP
DebugSerial.println ( “End of wait for setup start” );
#endif

if ( bI < 12 && bCmd == SETUP_BEGIN )
{
int nOnOff = HIGH;

digitalWrite ( MLinkLED, HIGH );
digitalWrite ( FrSkyLED, LOW );

DebugSerial.println ( “Setup loop start” );

while ( bCmd != SETUP_ESC )
{
switch ( bCmd )
{
case SETUP_BEGIN:
{
#ifdef DEBUG_SETUP
DebugSerial.println ( “Write info message to MLink serial” );
#endif

MLinkSerial_C3 |= 0x20;

MLinkSerial.write ( SETUP_BEGIN );
MLinkSerial.print ( “FryLink Version 1.0 from Burkhard R. Vogt, Muenster, Germany” );
MLinkSerial.write ( SETUP_END );
MLinkSerial.flush (); // make sure send is complete

MLinkSerial_C3 ^= 0x20;

MLinkSerial.clear(); // and clear the pipe

break;
}

case SETUP_READ: // The PC program will get the EEPROM infos
{
#ifdef DEBUG_SETUP
DebugSerial.println ( “Write setting to MLink serial” );
#endif

MLinkSerial_C3 |= 0x20;

MLinkSerial.write ( SETUP_READ );

for ( bI=0; bI<16; bI++)
{
byte bK, *bBytes = (byte *) &SensDef[bI];

#ifdef DEBUG_SETUP
DebugSerial.print ( bI );
DebugSerial.print ( ” – ” );
#endif

for ( bK=0; bK<8; bK++ )
{
#ifdef DEBUG_SETUP
DebugSerial.print ( bBytes[bK], HEX );
DebugSerial.print ( ” ” );
#endif

MLinkSerial.write ( bBytes[bK] );
}
#ifdef DEBUG_SETUP
DebugSerial.println ();
#endif
}

MLinkSerial.write ( SETUP_END );

MLinkSerial.flush (); // make sure send is complete

MLinkSerial_C3 ^= 0x20;

MLinkSerial.clear(); // and clear the pipe

break;
}

case SETUP_WRITE: // The PC program sends one new sensor info
{
#ifdef DEBUG_SETUP
DebugSerial.println ( “Read setting from MLink serial” );
#endif

byte bBytes[10];

for ( bI=0; bI<10; bI++ )
{
bBytes[bI] = MLinkSerial.read();

#ifdef DEBUG_SETUP
DebugSerial.print ( bBytes[bI], HEX );
DebugSerial.print ( ” ” );
#endif

}
#ifdef DEBUG_SETUP
DebugSerial.println ();
#endif

if ( bBytes[9] == SETUP_END )
{
byte bIdx = bBytes[0]; // Only for better reading …

#ifdef DEBUG_SETUP
DebugSerial.println ( “New settings OK” );
#endif

memcpy ( &SensDef[bIdx], bBytes + 1, 8 );

SetSensClass ( bIdx );

WriteSetting ( bIdx );
}
break;
}
}

bCmd = MLinkSerial.read ();

digitalWrite ( SetupLED, nOnOff );

nOnOff = nOnOff == HIGH ? LOW : HIGH;

delay ( 200 );
}
digitalWrite ( SetupLED, LOW );

#ifdef DEBUG_SETUP
DebugSerial.println ( “Setup loop end” );
#endif
}

// Delete all data in pipes …

MLinkSerial.clear();
FrSkySerial.clear();

digitalWrite ( MLinkLED, LOW );
digitalWrite ( FrSkyLED, LOW );

for ( bI=0; bI<(FrSkyPIdAnz*5); bI++ )
{
HandleFrSky ();
}

#ifdef DEBUG
DebugSerial.println ( “Function loop should start ;-)” );
#endif
}

// Some global variables for the FrSky serial handling

unsigned long ulFrSkyMsec = 0;
byte bFrSkySId = 0;
byte bFrSkyRead = 0;
byte bFrSkyByte[8];

// Saves the value read from FrSky in sensor struct for MLink request

void SaveValue ( unsigned int uTypID, short iValue )
{
for ( byte bI = 0; bI < 16; bI++ )
{
if ( SensDef[bI].uTypID == uTypID )
{
SensDef[bI].iValue = iValue;

if ( SensDef[bI].bAlert )
{
if ( iValue < SensDef[bI].iMin )
{
SensDef[bI].bBuzz = 1;
}
else
{
if ( iValue > SensDef[bI].iMax )
{
SensDef[bI].bBuzz = 1;
}
else
{
SensDef[bI].bBuzz = 0;
}
}
}
SensDef[bI].ulMsec = millis ();

#ifdef DEBUG_FRSKY
DebugSerial.print ( “FrSky input value saved as ” );
DebugSerial.println ( bI );
#endif
}
}
return;
}

// Function to handle the FrSky serial communication

void HandleFrSky ()
{
if ( FrSkySerial.available() )
{
// Set LED on when data reading …

if ( ! bFrSkyRead ) digitalWrite ( FrSkyLED, HIGH );

bFrSkyByte[bFrSkyRead] = FrSkySerial.read();

// Check for FrSky stuffing spezial char

if ( bFrSkyByte[bFrSkyRead] == 0x7D )
{
while ( ! FrSkySerial.available() ) { ; }

// Read the “real” value

bFrSkyByte[bFrSkyRead] = FrSkySerial.read() | 0x20;
}

#ifdef DEBUG_FRSKY
DebugSerial.print ( bFrSkyByte[bFrSkyRead], HEX );
DebugSerial.print ( ” ” );
#endif

if ( bFrSkyRead++ < 7 ) return;

#ifdef DEBUG_FRSKY
DebugSerial.println ();
#endif

// Parameter set is complete

if ( bFrSkyByte[0] == 0x10 )
{
unsigned short uTypID = ( bFrSkyByte[2] << 8 ) + bFrSkyByte[1];

#ifdef DEBUG_FRSKY
DebugSerial.print ( “TypID: ” );
DebugSerial.println ( uTypID, HEX );
#endif

switch ( uTypID )
{
case 0x0110: // vario | 0.01m/s | int32
break;

case 0x0200: // current | 0.01A | uint32
{
unsigned long uValue;

memcpy ( &uValue, bFrSkyByte + 3, 4 );

#ifdef DEBUG_FRSKY
DebugSerial.print ( “Current 0.1A: ” );
DebugSerial.println ( uValue / 10 );
#endif

SaveValue ( uTypID, uValue / 10 );

break;
}

case 0x0210: // voltage | 0.01V | uint32
{
unsigned long uValue;

memcpy ( &uValue, bFrSkyByte + 3, 4 );

#ifdef DEBUG_FRSKY
DebugSerial.print ( “Voltage 0.1V: ” );
DebugSerial.println ( uValue / 10 );
#endif

SaveValue ( uTypID, uValue / 10 );

break;
}

case 0x0300: // voltage | 0.01V | uint32
{
unsigned long uValue;

memcpy ( &uValue, bFrSkyByte + 3, 4 );

byte bCell = uValue & 0x0F; // First cell of two

uValue >>= 4;

byte bCells = uValue & 0x0F; // Num of cells

uValue >>= 4;

#ifdef DEBUG_FRSKY
DebugSerial.print ( “Voltage Cell ” );
DebugSerial.print ( bCell );
DebugSerial.print ( ” 0.1V: ” );
DebugSerial.println ( ( uValue & 0x0FFF ) / 50 );
#endif

SaveValue ( uTypID + bCell, ( uValue & 0x0FFF ) / 50 );

bCell ++;

if ( bCell < bCells )
{
uValue >>= 12;

#ifdef DEBUG_FRSKY
DebugSerial.print ( “Voltage Cell ” );
DebugSerial.print ( bCell );
DebugSerial.print ( ” 0.1V: ” );
DebugSerial.println ( ( uValue & 0x0FFF ) / 50 );
#endif

SaveValue ( uTypID + bCell, ( uValue & 0x0FFF ) / 50 );
}
break;
}

case 0x0500: // RPM | 1rpm | uint32
break;

case 0x0600: // capacity used | 1mah | uint32
break;

case 0x0610: // altitude | 1m | int32
break;

case 0x0830: // gps speed | 1km/h | uint32
break;

#ifdef DEBUG_FRSKY
default:
DebugSerial.println ( “Wrong TypID!” );
#endif
}
}
else
{
#ifdef DEBUG_FRSKY
DebugSerial.println ( “Wrong byte input!” );
#endif
}
bFrSkyRead = 0;
}

if ( ulFrSkyMsec < ( millis () – FRSKY_POLLING ) )
{
ulFrSkyMsec = millis ();

FrSkySerial_C3 |= 32;
FrSkySerial.write( 0x7E );
FrSkySerial.write( FrSkyPId[bFrSkySId] );
FrSkySerial.flush(); // make shure send is complete
FrSkySerial_C3 ^= 32;

#ifdef DEBUG_FRSKY
DebugSerial.print ( “Sending FrSky request ” );
DebugSerial.print ( bFrSkySID );
DebugSerial.print ( ” ” );
DebugSerial.print ( FrSkyPID[bFrSkySID], HEX );
DebugSerial.println ();
#endif

if ( bFrSkySId++ > FrSkyPIdAnz ) bFrSkySId = 0;

bFrSkyRead = 0;

digitalWrite ( FrSkyLED, LOW );
}
}

// Function to handle the MLink serial communication

void HandleMLink ()
{
byte bMLinkID = MLinkSerial.read();

if ( bMLinkID < 16 )
{
#ifdef DEBUG_MLINK
DebugSerial.print ( bMLinkID, HEX );
DebugSerial.print ( ” ” );
#endif

delay ( 3 );

// For secure, if another sensor uses the same ID

if ( ! MLinkSerial.available() )
{
// Is this sensor ID defined?

if ( SensDef[bMLinkID].uTypID )
{
digitalWrite ( MLinkLED, HIGH );

byte bRet = ( bMLinkID << 4 ) + SensDef[bMLinkID].bClass;
short iVal;

// Check if the last response is to old !

if ( SensDef[bMLinkID].ulMsec < ( millis () – FRSKY_POLLING * 100 ) )
{
iVal = 0x8000;
}
else
{
iVal = SensDef[bMLinkID].iValue * 2;

if ( SensDef[bMLinkID].bAlert ) iVal += SensDef[bMLinkID].bBuzz;
}

MLinkSerial_C3 |= 0x20;

MLinkSerial.write ( bRet ); // send sensor address and typ
MLinkSerial.write ( lowByte ( iVal ) );
MLinkSerial.write ( highByte ( iVal ) );
MLinkSerial.flush (); // make sure send is complete

MLinkSerial_C3 ^= 0x20;

#ifdef DEBUG_MLINK
DebugSerial.print ( “MLink response” );
#endif

digitalWrite ( MLinkLED, LOW );
}
}
else
{
byte bReq = 0;

while ( MLinkSerial.available() )
{
#ifdef DEBUG_MLINK
DebugSerial.print ( MLinkSerial.read(), HEX );
DebugSerial.print ( ” ” );
#else
MLinkSerial.read();
#endif

if ( bReq++ > 2 ) break;
}
}
}
else
{
#ifdef DEBUG_MLINK
DebugSerial.print ( “!- ” );
DebugSerial.print ( bMLinkID, HEX );
DebugSerial.print ( ” ” );
#endif

while ( MLinkSerial.available() )
{
#ifdef DEBUG_MLINK
DebugSerial.print ( MLinkSerial.read(), HEX );
DebugSerial.print ( ” ” );
#else
MLinkSerial.read();
#endif
}
}
#ifdef DEBUG_MLINK
DebugSerial.println ();
#endif
}

// The endless loop

void loop ()
{
while ( MLinkSerial.available() ) { HandleMLink (); }

HandleFrSky (); // Time for the FrSky S.Port
}

 

The MSB GPS Sensor

I had also been able to buy by Multiplex; yes everyone can ๐Ÿ˜‰
Since I had been involved in the development of an XCSoar Varios based on a Kobo Ebook Reader ever with GPS receivers, the components were virtually on my crafting table.
After initialization, the current position and altitude are determined so that the following values can be transmitted after the start (speed greater than 5 km / h)

  • Speed (I actually built it for that
  • Height
  • Distance from the stance position
  • Satellites in view (to be received)
  • … and the maximum values

The sensor on the tail boom with GPS module PA6H from Exp-Tech. The also connected UniSens-E current and voltage and speed sensor from SM Modellbau is easy to recognize.

If someone wants to build something like this here are some lines of code of the corresponding Arduino development environment

/*
* Multiplex GPS Sensor
*
* from Burkhard Vogt, Muenster, Westfalia, Germany
*
* Connections:
* Vin – Input (Bus) power by RX of MPX
* Gnd
* A1 – Serial wire MPX
* A3 – Signal LED (green)
* A6 – GPS LED (red)
* A10 – TX GPS
*/

#include <EEPROM.h>
#include <string.h>
#include <ctype.h>
#include <math.h>

const double M_RO = 180.0 / M_PI;

#define DebSerial Serial
#define MpxSerial Serial1
#define GpsSerial Serial2

#define MpxSerial_C1 UART0_C1
#define MpxSerial_C3 UART0_C3

// Some defines for the serial communication

#define MPX_DELAY 0 // Time between request and answer

// Some define for the start position

#define POS_DELAY 20 // Position sentence to wait for start
#define MID_DELAY 5 // Position sentence to middel (must lower than wait)

// the minimal speed to start

#define MIN_SPEED 5.0 // Minimum start speed

// Some global variables for the LEDs

const byte GpsLED = 3; // The yellow GPS LED
const byte MpxLED = 6; // The green Mpx LED

/* Sensor definitions

Types are:
– 01 – Speed
– 02 – Hight
– 03 – Distance
– 04 – Sattelites in view
– 05 – Max. Speed
– 06 – Max. Hight
– 07 – Max. Distance
– 08 – Max. Sattelites in view */

struct Sensor
{
byte bTyp;
byte bAlert;
short iValue;
byte bClass;
byte bBuzz;
}
SensDef[16];

byte SensRef[8]; // Reference for the real values (types)

// #define DEBUG
// #define DEBUG_SETUP
// #define DEBUG_MPX
// #define DEBUG_GPS

// Startadresse der Sensorausgabe
#define SADR 7

void setup()
{
#ifdef DEBUG
while ( ! DebSerial ) { ; }
DebSerial.begin ( 9600 );
DebSerial.println ( “Program and funktion setup start” );
#endif

for ( byte bI=0; bI<10; bI++ )
{
SensDef[bI].bTyp = 0;
}

SensDef[SADR+0].bTyp = 1; // Speed
SensDef[SADR+0].bAlert = 0; // No alarm
SensDef[SADR+0].iValue = 0x8000; // No value
SensDef[SADR+0].bClass = 4; // Speed 0,1km/h
SensDef[SADR+0].bBuzz = 0; // No buzzer

SensDef[SADR+1].bTyp = 2; // Hight
SensDef[SADR+1].bAlert = 0; // No alarm
SensDef[SADR+1].iValue = 0x8000; // No value
SensDef[SADR+1].bClass = 8; // Hight 1m
SensDef[SADR+1].bBuzz = 0; // No buzzer

SensDef[SADR+2].bTyp = 3; // Distance
SensDef[SADR+2].bAlert = 0; // No alarm
SensDef[SADR+2].iValue = 0x8000; // No value
SensDef[SADR+2].bClass = 8; // short distance 1m
SensDef[SADR+2].bBuzz = 0; // No buzzer

SensDef[SADR+3].bTyp = 4; // Sattelites
SensDef[SADR+3].bAlert = 0; // No alarm
SensDef[SADR+3].iValue = 0x8000; // No value
SensDef[SADR+3].bClass = 15; // Num. Sattelites
SensDef[SADR+3].bBuzz = 0; // No buzzer

SensDef[SADR+4].bTyp = 5; // Speed
SensDef[SADR+4].bAlert = 0; // No alarm
SensDef[SADR+4].iValue = 0x8000; // No value
SensDef[SADR+4].bClass = 4; // Speed 0,1km/h
SensDef[SADR+4].bBuzz = 0; // No buzzer

SensDef[SADR+5].bTyp = 8; // Distance
SensDef[SADR+5].bAlert = 0; // No alarm
SensDef[SADR+5].iValue = 0x8000; // No value
SensDef[SADR+5].bClass = 8; // Short distance 1m
SensDef[SADR+5].bBuzz = 0; // No buzzer

for ( byte bI=0; bI<8; bI++ )
{
SensRef[bI] = 0;
}

for ( byte bI=0; bI<16; bI++ )
{
if ( SensDef[bI].bTyp ) SensRef[SensDef[bI].bTyp-1] = bI + 1;
}

// Setup the pin states

pinMode ( MpxLED, OUTPUT );
pinMode ( GpsLED, OUTPUT );

// Setup the serial communication

MpxSerial.begin ( 38400 );
GpsSerial.begin ( 9600 );

MpxSerial_C1 |= 0x20 + 0x80; // Single wire mode

for ( byte bI=0; bI<6; bI++)
{
byte bLed = bI % 2;

digitalWrite ( MpxLED, bLed == 0 ? HIGH : LOW );
digitalWrite ( GpsLED, bLed == 1 ? HIGH : LOW );

delay ( 500 );
}

digitalWrite ( MpxLED, LOW );
digitalWrite ( GpsLED, LOW );

MpxSerial.clear();
GpsSerial.clear();

#ifdef DEBUG
DebSerial.println ( “Function loop should start ;-)” );
#endif
}

char GpsBuf[256];
byte GpsIdx = 0;

static const char Hex[] = “0123456789ABCDEF”;

double GetPos ( char *pStr, byte bLen )
{
char cDeg[4];
byte bI;

for ( bI=0; bI<bLen; bI++ )
{
if ( pStr[bI] == 0 ) return ( 0.0 );

cDeg[bI] = pStr[bI];
}

cDeg[bI] = 0;

double dDeg = atof ( cDeg );
double dMin = atof ( pStr + bLen );

return ( dDeg + dMin / 60.0 );
}

float fHgt, fHgtS = 0.0;
double dLat, dLon, dLatS = 0.0, dLonS = 0.0, dRad;
float fHight, fHightM = 0.0, fSpeed, fSpeedM = 0.0, fDist, fDistM = 0.0;
byte bFix = 0, bSat = 0, bSatM = 0, bVal = 0;

byte bMpxLED = 0;
byte bGpsLED = 0;
byte bPosCnt = 0;

// Function to handle the Mpx serial communication

void HandleMpx ()
{
byte bMLinkID = MpxSerial.read();

if ( bMLinkID < 16 )
{
#ifdef DEBUG_MPX
DebSerial.print ( bMLinkID, HEX );
DebSerial.print ( ” ” );
#endif

delay ( MPX_DELAY );

if ( ! bMLinkID )
{
if ( bMpxLED )
{
digitalWrite ( MpxLED, LOW );

bMpxLED = 0;
}
else
{
digitalWrite ( MpxLED, HIGH );

bMpxLED = 1;
}
}
if ( bMLinkID == 7 )
{
if ( ! bFix || ! bSat || ! bVal || bPosCnt < POS_DELAY )
{
digitalWrite ( MpxLED, LOW );

bMpxLED = 0;
}
}

// For secure, if another sensor uses the same ID

if ( ! MpxSerial.available() )
{
// Is this sensor ID defined?

if ( SensDef[bMLinkID].bTyp )
{
byte bRet = ( bMLinkID << 4 ) + SensDef[bMLinkID].bClass;

short iVal;

if ( bFix && bSat && bVal && bPosCnt >= POS_DELAY )
{
iVal = SensDef[bMLinkID].iValue * 2;
}
else
{
iVal = 0x8000;
}

if ( SensDef[bMLinkID].bAlert ) iVal += SensDef[bMLinkID].bBuzz;

MpxSerial_C3 |= 0x20;

MpxSerial.write ( bRet ); // send sensor address and typ
MpxSerial.write ( lowByte ( iVal ) );
MpxSerial.write ( highByte ( iVal ) );
MpxSerial.flush (); // make sure send is complete

MpxSerial_C3 ^= 0x20;

#ifdef DEBUG_MPX
DebSerial.print ( “MLink response” );
#endif
}
}
else
{
byte bReq = 0;

while ( MpxSerial.available() )
{
#ifdef DEBUG_MPX
DebSerial.print ( MpxSerial.read(), HEX );
DebSerial.print ( ” ” );
#else
MpxSerial.read();
#endif

if ( bReq++ > 2 ) break;
}
}
}
else
{
#ifdef DEBUG_MPX
DebSerial.print ( “!- ” );
DebSerial.print ( bMLinkID, HEX );
DebSerial.print ( ” ” );
#endif

#ifdef DEBUG_MPX
while ( MpxSerial.available() )
{
DebSerial.print ( MpxSerial.read(), HEX );
DebSerial.print ( ” ” );
}
#else
if ( MpxSerial.available() ) MpxSerial.clear();
#endif
}
#ifdef DEBUG_MPX
DebSerial.println ();
#endif
}

void SaveVal ( byte bTyp, short iVal )
{
if ( ! SensRef[bTyp-1] ) return;

SensDef[SensRef[bTyp-1]-1].iValue = iVal;
}

// Function to handle the GPS serial communication

void HandleGps ()
{
GpsBuf[GpsIdx] = GpsSerial.read();

if ( GpsBuf[GpsIdx] == 255 ) return;

if ( GpsBuf[GpsIdx] == 10 || GpsBuf[GpsIdx] == 13 )
{
if ( GpsIdx > 3 )
{
#ifdef DEBUG_GPS
GpsBuf[GpsIdx+1] = 0;
#endif

if ( GpsBuf[GpsIdx-3] != ‘*’ ) return; // Wrong format!

byte bI, CRC = 0; // Calculate the CRC and check

for ( bI=1; bI<(GpsIdx-3); bI++ ) CRC = CRC ^ GpsBuf[bI] ;

if ( GpsBuf[GpsIdx-2] != Hex[(CRC & 0xf0) >> 4] ||
GpsBuf[GpsIdx-1] != Hex[(CRC & 0x0f)] ) return;

#ifdef DEBUG_GPS
DebSerial.println ( GpsBuf );
#endif

byte bCmd = 0;

for ( bI=0; bI<2; bI++ )
{
char cCmd[] = { “GPGGA,GPRMC,” };
byte bK;

for ( bK=0; bK<6; bK++ )
{
if ( GpsBuf[bK+1] != cCmd[bI*6+bK] ) break;
}

if ( bK == 6 )
{
bCmd = bI * 20 + 20;

break;
}
}

if ( bCmd )
{
byte bStart = 7;
byte bParam = 0;

for ( bI=bStart; bI<(GpsIdx-2); bI++ )
{
if ( GpsBuf[bI] == ‘,’ || GpsBuf[bI] == ‘*’ )
{
GpsBuf[bI] = 0;
bParam++;

switch ( bCmd + bParam )
{
case 41: if ( bGpsLED )
{
digitalWrite ( GpsLED, LOW );

bGpsLED = 0;
}
else
{
if ( bFix && bSat && bVal )
{
digitalWrite ( GpsLED, HIGH );

bGpsLED = 1;
}
}
break;

case 42: if ( GpsBuf[bStart] == ‘A’ )
{
bVal = 1;
}
else
{
bVal = 0;
}

break;

case 22:
case 43: dLat = GetPos ( GpsBuf + bStart, 2 );

#ifdef DEBUG_GPS
DebSerial.print ( “Lat ” );
DebSerial.println ( dLat, 4 );
#endif

break;

case 23:
case 44: if ( GpsBuf[bStart] == ‘S’ ) dLat *= -1.0;

break;

case 24:
case 45: dLon = GetPos ( GpsBuf + bStart, 3 );

#ifdef DEBUG_GPS
DebSerial.print ( “Lon ” );
DebSerial.println ( dLon, 4 );
#endif

break;

case 25:
case 46: if ( GpsBuf[bStart] == ‘W’ ) dLon *= -1.0;

break;

case 26: bFix = atoi ( GpsBuf + bStart );

break;

case 27: bSat = atoi ( GpsBuf + bStart );

if ( bSat > bSatM ) bSatM = bSat;

SaveVal ( 4, min ( bSat, 10 ) );
SaveVal ( 8, min ( bSatM, 10 ) );

#ifdef DEBUG_GPS
DebSerial.print ( “Sats ” );
DebSerial.println ( bSat );
#endif

break;

case 29: fHgt = atof ( GpsBuf + bStart );

if ( bFix && bSat && bVal && bPosCnt >= POS_DELAY )
{
fHight = fHgt – fHgtS;

if ( fHight > fHightM ) fHightM = fHight;

#ifdef DEBUG_GPS
DebSerial.print ( “Hight ” );
DebSerial.println ( fHight, 1 );
#endif
}
else
{
fHight = 0x8000;
}

SaveVal ( 2, max ( 0, fHight ) );
SaveVal ( 6, fHightM );
break;

case 47: if ( bFix && bSat && bVal && bPosCnt >= POS_DELAY )
{
fSpeed = atof ( GpsBuf + bStart ) * 1.852;

#ifdef DEBUG_GPS
DebSerial.print ( “Speed ” );
DebSerial.println ( fSpeed, 2 );
#endif

if ( fSpeed < MIN_SPEED )
{
fSpeed = 0.0;
}
else
{
if ( fSpeed > fSpeedM ) fSpeedM = fSpeed;
}
}
else
{
fSpeed = 0x8000;
}

SaveVal ( 1, fSpeed * 10 );
SaveVal ( 5, fSpeedM * 10 );

#ifdef DEBUG_GPS
case 21:
case 28:
case 48:
case 49:
break;
default: DebSerial.print ( bCmd + bParam );
DebSerial.print ( “-” );
DebSerial.println ( GpsBuf+bStart );
#endif
}
if ( bParam > 8 ) break; // No more interesting params …

bStart = bI + 1;
}
}

if ( bFix && bSat && bVal )
{
if ( bPosCnt < POS_DELAY )
{
bPosCnt ++;

if ( bPosCnt > ( POS_DELAY – MID_DELAY ) )
{
#ifdef DEBUG_GPS
DebSerial.print ( “Save start position … ” );
DebSerial.println ( bPosCnt – POS_DELAY + MID_DELAY );
#endif

fHgtS += fHgt / MID_DELAY;
dLatS += dLat / MID_DELAY;
dLonS += dLon / MID_DELAY;

if ( bPosCnt == POS_DELAY )
{
dRad = 111300 * cos ( dLatS / M_RO );

#ifdef DEBUG_GPS
DebSerial.println ( “Startpoint” );
DebSerial.print ( “Lat ” );
DebSerial.println ( dLatS, 4 );
DebSerial.print ( “Lon ” );
DebSerial.println ( dLonS, 4 );
DebSerial.print ( “Hight ” );
DebSerial.println ( fHgtS, 1 );
DebSerial.print ( “Rad m ” );
DebSerial.println ( dRad, 0 );
#endif
}
}
}
else
{
#ifdef DEBUG_GPS
DebSerial.println ( “Calculate distance” );
#endif

double dX = ( dLon – dLonS ) * dRad;
double dY = ( dLat – dLatS ) * 111300;

fDist = sqrt ( dX * dX + dY * dY );

if ( fDist <= 2000 )
{
if ( fDist > fDistM ) fDistM = fDist;
}
else
{
fDist = 0x8000;
}

SaveVal ( 3, fDist );
SaveVal ( 7, fDistM );

#ifdef DEBUG_GPS
DebSerial.print ( “Dist ” );
DebSerial.println ( fDist, 2 );
#endif
}
}
}
}
GpsIdx = 0;
}
else
{
if ( GpsBuf[0] == ‘$’ ) GpsIdx++;

if ( GpsIdx > 255 ) GpsIdx = 0;
}
}

void loop ()
{
if ( MpxSerial.available() ) HandleMpx ();

if ( GpsSerial.available() ) HandleGps ();
}

 

The MSB Sensor Logger

I just wanted to develop a logger that I install directly into my transmitter. There I still had my beloved MC 3030. But this has three very decisive disadvantages:

  • You are hardware dependent!
    It would not work with my new transmitter …
  • But much more decisive: the polling rate!
    So the number of recorded sensor values โ€‹โ€‹during a time unit.
    On the MSB bus, the sensors are polled much more often than they are transmitted to the transmitter.
  • Availability depends on the reception of the sender
    I do not want to talk about it, but if the receiver (transmitter or receiver module) or receiver (transmitter or receiver module) fails, the sensor data is gone. Of course, then the very interesting Link Quality Indicator (LQI) or even current and voltage, which could indicate a cause of failure.

So I designed the logger so that it just listened in on the MSB ๐Ÿ˜‰
In addition, for the first time, I was able to read and write from a connected micro SD card.
The logger processes a configuration file (minimum current and voltage values) when logging should start or end. Subsequently, after a defined number of sensor queries, it is checked which sensors and sensor types are actually connected to the MSB or which information is transmitted. From this the label for a CSV file can be created. The data sets are then sequentially written to this file – after polling each of the connected sensors.

Here again the GPS sensor on the tail boom and the MSB logger on the right side. The micro SD card on the left side of the MSB logger is a bit difficult to recognize.

If someone wants to build something like this here are some lines of code of the corresponding Arduino development environment

/*
* Multiplex Telemetry Display Logger
*
* from Burkhard Vogt, Munster, Westfalia, Germany
*
* Connections:
* Vin – Power by TX (HFM3) of MPX
* Gnd
* A1 – Signal wire MLINK
*
* SD card attached to SPI bus as follows:
* MOSI – pin 11
* MISO – pin 12
* CLK – pin 13
* CS – pin 4
*
* A1 – Signal wire MLINK
* A3 – Signal LED (green)
* A6 – Setup LED (yellow)
*/

#include <SPI.h>
#include <SD.h>

struct SensDef
{
byte bClass;
short iValue;
}
Sensor[16];

char szFileName[13];

// #define DEBUG
// #define DEBUG_FILE
// #define DEBUG_MLINK

// Serial interfaces used

#define Debug Serial
#define MLink Serial1
#define SENS_WAIT 100

const byte GreenLED = 3; // The green LED
const byte YellowLED = 6; // The yellow LED

byte bBuffer[20], bBufIdx = 0;
int iMinA = 20; // Min power to start (1/10A)
int iMinR = 500; // Min rmp to start
char cDeci = ‘,’; // Decimal in Excel CSV

void setup()
{
// Setup the pin states

pinMode ( GreenLED, OUTPUT );
pinMode ( YellowLED, OUTPUT );

digitalWrite ( GreenLED, HIGH );
digitalWrite ( YellowLED, HIGH );

// For best compatiblity with all SD cards

pinMode ( 4, INPUT_PULLUP );
pinMode ( 10, INPUT_PULLUP );

delay (1); // allow time for both pins to reach 3.3V

#ifdef DEBUG_FILE
Debug.begin ( 9600 );
while ( ! Serial ) {;}
#endif

#ifdef DEBUG_FILE
Debug.print ( “Initializing SD card …” );
#endif

if ( ! SD.begin ( 4 ) )
{
#ifdef DEBUG_FILE
Debug.println ( ” failed!” );
#endif
return;
}
#ifdef DEBUG_FILE
Debug.println ( ” done.” );
#endif

File fRoot = SD.open ( “/” );
File fEntry;

unsigned int uLastIdx = 0;

while ( fEntry = fRoot.openNextFile () )
{
if ( ! fEntry.isDirectory())
{
if ( strlen ( fEntry.name() ) == 12 )
{
strcpy ( szFileName, fEntry.name() );

if ( ! strncmp ( szFileName, “LOG”, 3 ) &&
! strncmp ( szFileName + 8, “.CSV”, 4 ) )
{
unsigned int uIdx = atoi ( szFileName + 3 );

if ( uLastIdx < uIdx ) uLastIdx = uIdx;
}
}
}
fEntry.close();
}

sprintf ( szFileName, “LOG%05u.CSV”, ++uLastIdx );

#ifdef DEBUG_FILE
Debug.print ( “Filename ” );
Debug.println ( szFileName );
#endif

File SetFile = SD.open ( “SETTINGS.TXT”, FILE_READ );

if ( SetFile )
{
byte bByte;

do
{
bByte = SetFile.read();

if ( bByte == 0 ||
bByte == 0xFF ||
bByte == ‘\n’ ||
bByte == ‘\r’ )
{
bBuffer[bBufIdx] = 0;

if ( strlen ( (char *) bBuffer ) )
{
#ifdef DEBUG_FILE
Debug.println ( (char *) bBuffer );
#endif

if ( strncmp ( (char *) bBuffer, “MinA=”, 5 ) == 0 )
{
iMinA = atoi ( (char *) bBuffer + 5 );
#ifdef DEBUG_FILE
Debug.println ( “MinA->” );
Debug.println ( iMinA );
#endif
}
else if ( strncmp ( (char *) bBuffer, “MinR=”, 5 ) == 0 )
{
iMinR = atoi ( (char *) bBuffer + 5 );
#ifdef DEBUG_FILE
Debug.println ( “MinR->” );
Debug.println ( iMinR );
#endif
}
else if ( strncmp ( (char *) bBuffer, “Deci=”, 5 ) == 0 )
{
cDeci = bBuffer[5];

if ( cDeci != ‘.’ || cDeci != ‘,’ ) cDeci = ‘,’;
#ifdef DEBUG_FILE
Debug.println ( “Deci” );
Debug.println ( cDeci );
#endif
}
}
bBufIdx = 0;
}
else
{
if ( bBufIdx < 18 )
{
bBuffer[bBufIdx++] = bByte;
}
}
} while ( bByte != 0xFF );

SetFile.close ();
}
else
{
File SetFile = SD.open ( “SETTINGS.TXT”, FILE_WRITE );

if ( SetFile )
{
Debug.println ( “MinA=20” );
Debug.println ( “MinR=500” );
Debug.println ( “Deci=,” );

SetFile.close ();
}
}

bBufIdx = 0;

MLink.begin ( 38400 );
MLink.clear ();

digitalWrite ( GreenLED, LOW );
}

byte bLastIdx = 0;
byte bWriteSet = 0;
byte bToggle = 0;
unsigned int uWait = 0;
unsigned long ulMillis;

void ToggleLED ( byte LEDon, byte LEDoff )
{
if ( bToggle )
{
bToggle = 0;

digitalWrite ( LEDon, LOW );
}
else
{
bToggle = 1;

digitalWrite ( LEDon, HIGH );
}
digitalWrite ( LEDoff, LOW );
}

void HandleOutput ( void )
{
if ( uWait > SENS_WAIT )
{
if ( bWriteSet )
{
ToggleLED ( GreenLED, YellowLED );

File LogFile = SD.open ( szFileName, FILE_WRITE );

if ( LogFile )
{
char szOutput[12];

unsigned long ulDM = ( millis () – ulMillis ) / 100; // 1/10 sek
unsigned int uiSM = ulDM % 10;
ulDM /= 10;
unsigned int uiSec = ulDM % 60;
ulDM /= 60;
unsigned int uiMin = ulDM % 60;
ulDM /= 60;

sprintf ( szOutput, “%02lu:%02u:%02u%c%01u;”, ulDM, uiMin, uiSec, cDeci, uiSM );

LogFile.print ( szOutput );

for ( byte bI=0; bI<16; bI++ )
{
switch ( Sensor[bI].bClass )
{
case 1:
case 2:
case 3:
case 4:
case 6:
case 7:
case 13:

sprintf ( szOutput, “%i%c%i;”, Sensor[bI].iValue / 10, cDeci, Sensor[bI].iValue % 10 );
LogFile.print ( szOutput );
break;

case 5:

if ( Sensor[bI].iValue < 0 )
{
LogFile.print ( abs ( Sensor[bI].iValue * 10 ) );
}
else
{
LogFile.print ( Sensor[bI].iValue * 100 );
}
LogFile.print ( “;” );
break;

case 8:
case 9:
case 10:
case 11:
case 12:
case 14:
case 15:

LogFile.print ( Sensor[bI].iValue );
LogFile.print ( “;” );
}
}
LogFile.println ();
LogFile.close ();
}
}
else
{
ToggleLED ( YellowLED, GreenLED );
}
}
else
{
if ( uWait == SENS_WAIT )
{
ToggleLED ( GreenLED, YellowLED );

ulMillis = millis ();

File LogFile = SD.open ( szFileName, FILE_WRITE );

if ( LogFile )
{
LogFile.print ( “Zeit (1/10s);” );

for ( byte bI=0; bI<16; bI++ )
{
switch ( Sensor[bI].bClass )
{
case 1: LogFile.print ( “Spannung (V);” ); break;
case 2: LogFile.print ( “Strom (A);” ); break;
case 3: LogFile.print ( “Steigen/Sinken (m/s);” ); break;
case 4: LogFile.print ( “Geschwindigkeit (km/h);” ); break;
case 5: LogFile.print ( “Drehzahl (1/min);” ); break;
case 6: LogFile.print ( “Temperatur (\xB0\x43);” ); break;
case 7: LogFile.print ( “Richtung (Grad \xB0);” ); break;
case 8: LogFile.print ( “H\xF6he/Distanz (m);” ); break;
case 9: LogFile.print ( “F\xFCllstand (%);” ); break;
case 10: LogFile.print ( “LQI (%);” ); break;
case 11: LogFile.print ( “Stromverbrauch (mAh);” ); break;
case 12: LogFile.print ( “Fl\xFCssigkeiten (mL);” ); break;
case 13: LogFile.print ( “Distanz (km);” ); break;
case 14: LogFile.print ( “unbekannt;” ); break;
case 15: LogFile.print ( “Wert;” );
}
}
LogFile.println ();
LogFile.close ();
}
}
else
{
ToggleLED ( YellowLED, GreenLED );
}
uWait ++;
}
}

void HandleSensor ( void )
{
byte bIdx = bBuffer[0];
byte bClass = bBuffer[1] & 0x0F;

// If wait long enough
// Write the header or the values …

if ( bLastIdx > bIdx )
{
HandleOutput ();

bWriteSet = 0; // Reset Outputflag
}

bLastIdx = bIdx;

// Save the new sensor values

if ( uWait <= SENS_WAIT )
{
Sensor[bIdx].bClass = bClass;
}
else
{
if ( Sensor[bIdx].bClass != bClass ) return;
}

memcpy ( &Sensor[bIdx].iValue, bBuffer + 2, 2 );

Sensor[bIdx].iValue /= 2;

if ( Sensor[bIdx].bClass == 2 )
{
if ( iMinA <= Sensor[bIdx].iValue ) bWriteSet = 1;
}
else if ( Sensor[bIdx].bClass == 5 )
{
if ( Sensor[bIdx].iValue < 0 )
{
if ( iMinR <= abs ( Sensor[bIdx].iValue * 10 ) ) bWriteSet = 1;
}
else
{
if ( iMinR <= ( Sensor[bIdx].iValue * 100 ) ) bWriteSet = 1;
}
}

#ifdef DEBUG
if ( bWriteSet )
{
Debug.print ( bBuffer[0], HEX );
Debug.print ( ” ” );
Debug.print ( bBuffer[1], HEX );
Debug.print ( ” ” );
Debug.print ( bBuffer[2], HEX );
Debug.print ( ” ” );
Debug.print ( bBuffer[3], HEX );
Debug.println ();
}
#endif

return;
}

void loop()
{
if ( ! MLink.available() ) return; // Nothing to do !

bBuffer[0] = MLink.read();

if ( bBuffer[0] <= 0x0F )
{
#ifdef DEBUG_MLINK
Debug.print ( bBuffer[0], HEX );
Debug.print ( ” ” );
#endif

// For secure, we wait for a full data set

delay ( 4 );

if ( MLink.available() )
{
bBufIdx = 1;

while ( MLink.available() )
{
bBuffer[bBufIdx] = MLink.read();

#ifdef DEBUG_MLINK
Debug.print ( bBuffer[bBufIdx], HEX );
Debug.print ( ” ” );
#endif

if ( ++bBufIdx > 3 ) break;
}

if ( bBufIdx == 4 && bBuffer[0] == ( bBuffer[1] >> 4 ) )
{
#ifdef DEBUG_MLINK
Debug.print ( “OK” );
#endif

HandleSensor ();
}
}
}
else if ( bBuffer[0] == 0x5A )
{
#ifdef DEBUG_MLINK
Debug.print ( “!- Reset” );
#endif
}
else if ( bBuffer[0] >= 0x80 && bBuffer[0] <= 0x8F )
{
#ifdef DEBUG_MLINK
Debug.print ( “!- System Function” );
#endif

MLink.clear();
}
else
{
#ifdef DEBUG_MLINK
Debug.print ( “!- ” );
Debug.print ( bBuffer[0], HEX );
Debug.print ( ” ” );
while ( MLink.available() )
{
Debug.print ( MLink.read(), HEX );
Debug.print ( ” ” );
}
#else
MLink.clear();
#endif
}
#ifdef DEBUG_MLINK
Debug.println ();
#endif
}