@ -1,14 +1,28 @@
// flymaster-client.js
// WebSerial implementation of the Flymaster GPS protocol
class FlymasterClient {
// Make FlymasterClient available globally
window . FlymasterClient = class FlymasterClient {
/ * *
* Convert a Uint8Array to a hex string
* @ param { Uint8Array } bytes - The bytes to convert
* @ returns { string } - The hex string representation
* /
bytesToHexString ( bytes ) {
if ( ! bytes || bytes . length === 0 ) return '' ;
return Array . from ( bytes )
. map ( byte => byte . toString ( 16 ) . padStart ( 2 , '0' ) )
. join ( ' ' ) ;
}
constructor ( ) {
// Constants
this . BAUDRATE = 57600 ;
this . XON = 0x11 ;
this . XOFF = 0x13 ;
this . MAX _LINE = 90 ;
this . DOWN _TIMEOUT = 1000 ; // 1 second in milliseconds
this . DOWN _TIMEOUT = 1000 ; // 1 second in milliseconds (matching C++ implementation)
// Packet IDs
this . PACKET _FLIGHT _INFO = 0xa0a0 ;
@ -30,6 +44,7 @@ class FlymasterClient {
this . gpsunitid = 0 ;
this . saved _tracks = [ ] ;
this . selected _tracks = [ ] ;
this . flightInfo = null ;
}
/ * *
@ -44,7 +59,7 @@ class FlymasterClient {
dataBits : 8 ,
stopBits : 1 ,
parity : "none" ,
flowControl : "none" // We'll handle XON/XOFF in software
flowControl : "none"
} ) ;
// Get reader and writer
@ -83,13 +98,15 @@ class FlymasterClient {
/ * *
* Initialize the GPS device
* This matches the C ++ implementation in FlymasterGps : : init _gps ( )
* /
async initGps ( ) {
try {
// Get device information
const result = await this . sendCommand ( "PFMSNP" ) ;
this . gpsname = result [ 0 ] ;
// Compute unit id as a XOR of the result
// Compute unit id as a XOR of the result (matching C++ implementation)
this . gpsunitid = 0 ;
for ( let i = 0 ; i < result . length ; i ++ ) {
for ( let j = 0 ; j < result [ i ] . length ; j ++ ) {
@ -133,158 +150,78 @@ class FlymasterClient {
console . log ( ` Initialized GPS: ${ this . gpsname } , found ${ this . saved _tracks . length } tracks ` ) ;
return this . saved _tracks ;
} catch ( error ) {
console . error ( "Error in initGps:" , error ) ;
throw error ;
}
/ * *
* Generate NMEA command with checksum
* /
genCommand ( command , parameters = [ ] ) {
let data = '$' + command + ',' ;
let cksum = 0 ;
// Calculate checksum for command
for ( let i = 0 ; i < command . length ; i ++ ) {
cksum ^= command . charCodeAt ( i ) ;
}
cksum ^= ',' . charCodeAt ( 0 ) ;
// Add parameters and update checksum
for ( let i = 0 ; i < parameters . length ; i ++ ) {
data += parameters [ i ] ;
// Update checksum with parameter
for ( let j = 0 ; j < parameters [ i ] . length ; j ++ ) {
cksum ^= parameters [ i ] . charCodeAt ( j ) ;
}
data += ',' ;
cksum ^= ',' . charCodeAt ( 0 ) ;
}
// Add checksum as hexadecimal
data += '*' + cksum . toString ( 16 ) . padStart ( 2 , '0' ) . toUpperCase ( ) + '\r\n' ;
return data ;
}
/ * *
* Send a command and get response
* /
async sendCommand ( command , parameters = [ ] ) {
await this . sendSmplCommand ( command , parameters ) ;
return await this . receiveData ( command ) ;
}
/ * *
* Send a command without waiting for response
* Read a packet from the device
* This matches the C ++ implementation in FlymasterGps : : read _packet ( )
* /
async sendSmplCommand ( command , parameters = [ ] ) {
const cmdStr = this . genCommand ( command , parameters ) ;
const encoder = new TextEncoder ( ) ;
const data = encoder . encode ( cmdStr ) ;
await this . writer . write ( data ) ;
async readPacket ( ) {
try {
// Read initial packet data (at least header)
const { value : initialBytes , done : initialDone } = await this . reader . read ( ) ;
if ( initialDone || ! initialBytes || initialBytes . length < 3 ) { // At minimum: 2 bytes ID + 1 byte length
throw new Error ( "Failed to read packet header" ) ;
}
/ * *
* Read data from the device until a valid response is received
* /
async receiveData ( command ) {
const decoder = new TextDecoder ( ) ;
let received = 0 ;
let recv _cmd = "" ;
let param = "" ;
let result = [ ] ;
let incmd = false ;
let cksum = 0 ;
const startTime = Date . now ( ) ;
console . debug ( "Received initial bytes:" , this . bytesToHexString ( initialBytes ) ) ;
while ( Date . now ( ) - startTime < this . DOWN _TIMEOUT ) {
const { value , done } = await this . reader . read ( ) ;
if ( done ) break ;
for ( let i = 0 ; i < value . length ; i ++ ) {
const ch = value [ i ] ;
received ++ ;
// Ignore XON, XOFF
if ( ch === this . XON || ch === this . XOFF ) continue ;
// Extract packet ID (first 2 bytes)
const packetId = initialBytes [ 0 ] + ( initialBytes [ 1 ] << 8 ) ;
if ( ch === 0x24 ) { // '$'
incmd = true ;
cksum = 0 ;
param = "" ;
recv _cmd = "" ;
result = [ ] ;
continue ;
if ( packetId === this . PACKET _END _MARKER ) {
return { end : true } ;
}
if ( ! incmd ) continue ;
// Extract length (3rd byte)
const length = initialBytes [ 2 ] ;
if ( ch !== 0x2A ) // '*'
cksum ^= ch ;
// Calculate total expected packet size: 3 bytes header + data length + 1 byte checksum
const totalExpectedSize = 3 + length + 1 ;
if ( ch === 0x2C || ch === 0x2A ) { // ',' or '*'
if ( param . length ) {
if ( ! recv _cmd . length )
recv _cmd = param ;
else
result . push ( param ) ;
}
param = "" ;
// Create a buffer for the complete packet
let packetBytes = new Uint8Array ( totalExpectedSize ) ;
if ( ch === 0x2A ) { // '*'
// Read checksum (2 hex characters)
const cksum _s = String . fromCharCode ( value [ ++ i ] ) + String . fromCharCode ( value [ ++ i ] ) ;
const cksum _r = parseInt ( cksum _s , 16 ) ;
// Copy initial bytes to the packet buffer
let bytesReceived = Math . min ( initialBytes . length , totalExpectedSize ) ;
packetBytes . set ( initialBytes . slice ( 0 , bytesReceived ) , 0 ) ;
// Skip CR, LF
i += 2 ;
if ( cksum _r === cksum && recv _cmd === command )
return result ;
}
continue ;
// Continue reading until we have the complete packet
while ( bytesReceived < totalExpectedSize ) {
const { value : moreBytes , done : moreDone } = await this . reader . read ( ) ;
if ( moreDone ) {
throw new Error ( "Connection closed while reading packet data" ) ;
}
param += String . fromCharCode ( ch ) ;
}
if ( ! moreBytes || moreBytes . length === 0 ) {
continue ;
}
throw new Error ( "Timeout waiting for response" ) ;
}
console . debug ( "Received more bytes:" , this . bytesToHexString ( moreBytes ) ) ;
/ * *
* Read a binary packet from the device
* /
async readPacket ( ) {
// Read packet ID (2 bytes)
const { value : idBytes } = await this . reader . read ( ) ;
const packetId = idBytes [ 0 ] + ( idBytes [ 1 ] << 8 ) ;
// Calculate how many more bytes we need and how many we can copy
const bytesNeeded = totalExpectedSize - bytesReceived ;
const bytesToCopy = Math . min ( moreBytes . length , bytesNeeded ) ;
if ( packetId === this . PACKET _END _MARKER ) {
return { end : true } ;
// Copy the bytes to the packet buffer
packetBytes . set ( moreBytes . slice ( 0 , bytesToCopy ) , bytesReceived ) ;
bytesReceived += bytesToCopy ;
}
// Read length (1 byte)
const { value : lengthByte } = await this . reader . read ( ) ;
const length = lengthByte [ 0 ] ;
// Read data
let data = new Uint8Array ( length ) ;
let bytesRead = 0 ;
while ( bytesRead < length ) {
const { value } = await this . reader . read ( ) ;
const remaining = length - bytesRead ;
const bytesToCopy = Math . min ( remaining , value . length ) ;
console . debug ( "Complete packet assembled:" , this . bytesToHexString ( packetBytes ) ) ;
data . set ( value . slice ( 0 , bytesToCopy ) , bytesRead ) ;
bytesRead += bytesToCopy ;
// Extract data (next 'length' bytes after header)
const data = new Uint8Array ( length ) ;
for ( let i = 0 ; i < length ; i ++ ) {
data [ i ] = packetBytes [ 3 + i ] ;
}
// Read checksum
const { value : cksumByte } = await this . reader . read ( ) ;
const cksum = cksumByte [ 0 ] ;
// Extract checksum (last byte after data)
const cksum = packetBytes [ 3 + length ] ;
// Compute checksum
let c _cksum = length ;
@ -297,15 +234,31 @@ class FlymasterClient {
await this . writer . write ( new Uint8Array ( [ this . ACK _NEGATIVE ] ) ) ;
throw new Error ( "Data checksum error" ) ;
}
else {
await this . writer . write ( new Uint8Array ( [ this . ACK _POSITIVE ] ) ) ;
}
return { packetId , data } ;
} catch ( error ) {
console . error ( "Error in readPacket:" , error ) ;
throw error ;
}
}
/ * *
* Download a specific track
* Download a single track
* This matches the C ++ implementation in FlymasterGps : : download _strack ( )
* /
async downloadTrack ( trackIndex , progressCallback = null ) {
async downloadStrack ( trackIndex , progressCallback = null ) {
try {
const track = this . saved _tracks [ trackIndex ] ;
if ( ! track ) {
throw new Error ( ` Track ${ trackIndex } not found ` ) ;
}
// Reset flight info before downloading
this . flightInfo = null ;
const result = [ ] ;
// Format date for command
@ -329,15 +282,17 @@ class FlymasterClient {
const expCount = Math . floor ( ( track . endDate - track . startDate ) / 1000 ) ;
while ( true ) {
try {
const packet = await this . readPacket ( ) ;
// Check if end of transmission
if ( packet . end ) break ;
if ( packet . end ) {
break ;
}
if ( packet . packetId === this . PACKET _FLIGHT _INFO ) {
// Flight information packet
// We could parse this, but for now we'll just acknowledge it
this . flightInfo = this . parseFlightInfo ( packet . data ) ;
console . debug ( "Received flight info:" , this . flightInfo ) ;
}
else if ( packet . packetId === this . PACKET _KEY _POSITION ) {
// Key position packet
@ -372,31 +327,84 @@ class FlymasterClient {
result . push ( this . makePoint ( basePos , false ) ) ;
}
}
}
// Send positive acknowledgment
await this . writer . write ( new Uint8Array ( [ this . ACK _POSITIVE ] ) ) ;
return result ;
} catch ( error ) {
console . error ( "Error reading packet:" , error ) ;
break ;
console . error ( "Error in downloadStrack:" , error ) ;
throw error ;
}
}
/ * *
* Download all selected tracks
* This matches the C ++ implementation in FlymasterGps : : download _tracklog ( )
* /
async downloadTracklog ( progressCallback = null ) {
try {
const result = [ ] ;
// Sort tracks from higher index to lower (older date first)
// So that the resulting array is time sorted
this . selected _tracks . sort ( ( a , b ) => b - a ) ;
for ( let i = 0 ; i < this . selected _tracks . length ; i ++ ) {
const points = await this . downloadStrack ( this . selected _tracks [ i ] , progressCallback ) ;
result . push ( ... points ) ;
}
return result ;
} catch ( error ) {
console . error ( "Error in downloadTracklog:" , error ) ;
throw error ;
}
}
/ * *
* Parse a flight info packet
* /
parseFlightInfo ( data ) {
if ( ! data || data . length < 58 ) {
throw new Error ( "Invalid flight info data" ) ;
}
const view = new DataView ( data . buffer ) ;
// Extract string fields
const decoder = new TextDecoder ( 'ascii' ) ;
const compnum = decoder . decode ( data . slice ( 8 , 16 ) ) . replace ( /\0/g , '' ) ;
const pilotname = decoder . decode ( data . slice ( 16 , 31 ) ) . replace ( /\0/g , '' ) ;
const gliderbrand = decoder . decode ( data . slice ( 31 , 46 ) ) . replace ( /\0/g , '' ) ;
const glidermodel = decoder . decode ( data . slice ( 46 , 61 ) ) . replace ( /\0/g , '' ) ;
return {
sw _version : view . getUint16 ( 0 , true ) , // little-endian
hw _version : view . getUint16 ( 2 , true ) , // little-endian
serial : view . getUint32 ( 4 , true ) , // little-endian
compnum : compnum ,
pilotname : pilotname ,
gliderbrand : gliderbrand ,
glidermodel : glidermodel
} ;
}
/ * *
* Parse a key position packet
* /
parseKeyPosition ( data ) {
if ( ! data || data . length < 17 ) {
throw new Error ( "Invalid key position data" ) ;
}
const view = new DataView ( data . buffer ) ;
return {
fix : data [ 16 ] ,
latitude : view . getInt32 ( 0 , true ) , // little-endian
longitude : view . getInt32 ( 4 , true ) , // little-endian
gpsaltitude : view . getInt16 ( 8 , true ) , // little-endian
baro : view . getInt16 ( 10 , true ) , // little-endian
time : view . getUint32 ( 12 , true ) , // little-endian
fix : data [ 16 ]
time : view . getUint32 ( 12 , true ) // little-endian
} ;
}
@ -404,18 +412,25 @@ class FlymasterClient {
* Parse delta positions from a packet
* /
parseDeltaPositions ( data ) {
if ( ! data ) {
throw new Error ( "Invalid delta position data" ) ;
}
const deltas = [ ] ;
const DELTA _SIZE = 9 ; // Size of each delta structure
const DELTA _SIZE = 6 ; // Size of each delta structure
const view = new DataView ( data . buffer , data . byteOffset , data . byteLength ) ;
for ( let i = 0 ; i + DELTA _SIZE <= data . length ; i += DELTA _SIZE ) {
deltas . push ( {
const delta = {
fix : data [ i ] ,
latoff : new Int16Array ( data . buffer . slice ( i + 1 , i + 3 ) ) [ 0 ] ,
lonoff : new Int16Array ( data . buffer . slice ( i + 3 , i + 5 ) ) [ 0 ] ,
gpsaltoff : data [ i + 5 ] ,
baroff : data [ i + 6 ] ,
timeoff : new Uint16Array ( data . buffer . slice ( i + 7 , i + 9 ) ) [ 0 ]
} ) ;
latoff : view . getInt8 ( i + 1 ) ,
lonoff : view . getInt8 ( i + 2 ) ,
gpsaltoff : view . getInt8 ( i + 3 ) ,
baroff : view . getInt8 ( i + 4 ) ,
timeoff : data [ i + 5 ]
} ;
deltas . push ( delta ) ;
}
return deltas ;
@ -423,6 +438,7 @@ class FlymasterClient {
/ * *
* Convert a position to a trackpoint
* This matches the C ++ implementation in make _point ( )
* /
makePoint ( pos , newTrack ) {
return {
@ -436,79 +452,171 @@ class FlymasterClient {
}
/ * *
* Get list of available tracks
* Generate NMEA command with checksum
* This matches the C ++ implementation in NMEAGps : : gen _command ( )
* /
getTrackList ( ) {
return this . saved _tracks . map ( ( track , index ) => {
return {
index : index ,
startDate : track . startDate ,
endDate : track . endDate ,
duration : Math . floor ( ( track . endDate - track . startDate ) / 1000 )
} ;
} ) ;
genCommand ( command , parameters = [ ] ) {
let data = '$' + command + ',' ;
let cksum = 0 ;
// Calculate checksum for command
for ( let i = 0 ; i < command . length ; i ++ ) {
cksum ^= command . charCodeAt ( i ) ;
}
cksum ^= ',' . charCodeAt ( 0 ) ;
// Add parameters and update checksum
for ( let i = 0 ; i < parameters . length ; i ++ ) {
data += parameters [ i ] ;
// Update checksum with parameter
for ( let j = 0 ; j < parameters [ i ] . length ; j ++ ) {
cksum ^= parameters [ i ] . charCodeAt ( j ) ;
}
data += ',' ;
cksum ^= ',' . charCodeAt ( 0 ) ;
}
// Add checksum as hexadecimal
data += '*' + cksum . toString ( 16 ) . padStart ( 2 , '0' ) . toUpperCase ( ) + '\r\n' ;
return data ;
}
/ * *
* Select tracks for download
* Send a command and get response
* This matches the C ++ implementation in NMEAGps : : send _command ( )
* /
selectTracks ( indices ) {
this . selected _tracks = indices . sort ( ( a , b ) => b - a ) ; // Sort in descending order
async sendCommand ( command , parameters = [ ] ) {
await this . sendSmplCommand ( command , parameters ) ;
return await this . receiveData ( command ) ;
}
/ * *
* Download all selected tracks
* Send a command without waiting for response
* This matches the C ++ implementation in NMEAGps : : send _smpl _command ( )
* /
async downloadSelectedTracks ( progressCallback = null ) {
let allPoints = [ ] ;
async sendSmplCommand ( command , parameters = [ ] ) {
const cmdStr = this . genCommand ( command , parameters ) ;
const encoder = new TextEncoder ( ) ;
const data = encoder . encode ( cmdStr ) ;
await this . writer . write ( data ) ;
console . debug ( "Sent command" , cmdStr ) ;
}
/ * *
* Read data from the device until a valid response is received
* This matches the C ++ implementation in NMEAGps : : receive _data ( )
* /
async receiveData ( command ) {
const decoder = new TextDecoder ( ) ;
let received = 0 ;
let recv _cmd = "" ;
let param = "" ;
let result = [ ] ;
let incmd = false ;
let cksum = 0 ;
const startTime = Date . now ( ) ;
for ( const trackIndex of this . selected _tracks ) {
const points = await this . downloadTrack ( trackIndex , progressCallback ) ;
allPoints = allPoints . concat ( points ) ;
while ( Date . now ( ) - startTime < this . DOWN _TIMEOUT ) {
const { value , done } = await this . reader . read ( ) ;
if ( done ) {
break ;
}
return allPoints ;
for ( let i = 0 ; i < value . length ; i ++ ) {
const ch = value [ i ] ;
received ++ ;
// Ignore XON, XOFF
if ( ch === this . XON || ch === this . XOFF ) {
continue ;
}
if ( ch === 0x24 ) { // '$'
incmd = true ;
cksum = 0 ;
param = "" ;
recv _cmd = "" ;
result = [ ] ;
continue ;
}
// Example usage:
async function connectToFlymaster ( ) {
const client = new FlymasterClient ( ) ;
if ( ! incmd ) {
continue ;
}
try {
// Connect to device
const connected = await client . connect ( ) ;
if ( ! connected ) {
console . error ( "Failed to connect to device" ) ;
return ;
if ( ch !== 0x2A ) { // Not '*'
cksum ^= ch ;
}
// Initialize GPS
await client . initGps ( ) ;
if ( ch === 0x2C || ch === 0x2A ) { // ',' or '*'
if ( param . length ) {
if ( ! recv _cmd . length ) {
recv _cmd = param ;
} else {
result . push ( param ) ;
}
}
param = "" ;
// Get track list
const tracks = client . getTrackList ( ) ;
console . log ( "Available tracks:" , tracks ) ;
if ( ch === 0x2A ) { // '*'
// Read checksum (2 hex characters)
const cksum _s = String . fromCharCode ( value [ ++ i ] ) + String . fromCharCode ( value [ ++ i ] ) ;
const cksum _r = parseInt ( cksum _s , 16 ) ;
// Select tracks to download (e.g., the most recent one)
client . selectTracks ( [ 0 ] ) ;
// Skip CR, LF
i += 2 ;
// Download selected tracks with progress callback
const points = await client . downloadSelectedTracks ( ( current , total ) => {
const percent = Math . floor ( ( current / total ) * 100 ) ;
console . log ( ` Download progress: ${ percent } % ` ) ;
return true ; // Return false to cancel download
if ( cksum _r === cksum && recv _cmd === command ) {
return result ;
}
}
continue ;
}
param += String . fromCharCode ( ch ) ;
}
}
throw new Error ( ` Timeout waiting for response to ${ command } ` ) ;
}
/ * *
* Get list of available tracks
* /
getTrackList ( ) {
return this . saved _tracks . map ( ( track , index ) => {
return {
index : index ,
startDate : track . startDate ,
endDate : track . endDate ,
duration : Math . floor ( ( track . endDate - track . startDate ) / 1000 )
} ;
} ) ;
}
console . log ( ` Downloaded ${ points . length } points ` ) ;
/ * *
* Select tracks for download
* /
selectTracks ( indices ) {
this . selected _tracks = indices ;
}
// Disconnect
await client . disconnect ( ) ;
/ * *
* Download all selected tracks
* /
async downloadSelectedTracks ( progressCallback = null ) {
return await this . downloadTracklog ( progressCallback ) ;
}
return points ;
} catch ( error ) {
console . error ( "Error:" , error ) ;
await client . disconnect ( ) ;
/ * *
* Get the flight info data
* @ returns { Object | null } The flight info data or null if not available
* /
getFlightInfo ( ) {
return this . flightInfo ;
}
}