@ -1,35 +1,50 @@
// 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 ;
this . PACKET _KEY _POSITION = 0xa1a1 ;
this . PACKET _DELTA _POSITION = 0xa2a2 ;
this . PACKET _END _MARKER = 0xa3a3 ;
// Acknowledgment bytes
this . ACK _POSITIVE = 0xb1 ;
this . ACK _NEGATIVE = 0xb3 ;
// Serial port and streams
this . port = null ;
this . reader = null ;
this . writer = null ;
// Device information
this . gpsname = "" ;
this . gpsunitid = 0 ;
this . saved _tracks = [ ] ;
this . selected _tracks = [ ] ;
this . flightInfo = null ;
}
/ * *
@ -44,13 +59,13 @@ class FlymasterClient {
dataBits : 8 ,
stopBits : 1 ,
parity : "none" ,
flowControl : "none" // We'll handle XON/XOFF in software
flowControl : "none"
} ) ;
// Get reader and writer
this . reader = this . port . readable . getReader ( ) ;
this . writer = this . port . writable . getWriter ( ) ;
console . log ( "Connected to device" ) ;
return true ;
} catch ( error ) {
@ -67,100 +82,402 @@ class FlymasterClient {
await this . reader . releaseLock ( ) ;
this . reader = null ;
}
if ( this . writer ) {
await this . writer . releaseLock ( ) ;
this . writer = null ;
}
if ( this . port && this . port . readable ) {
await this . port . close ( ) ;
this . port = null ;
}
console . log ( "Disconnected from device" ) ;
}
/ * *
* Initialize the GPS device
* This matches the C ++ implementation in FlymasterGps : : init _gps ( )
* /
async initGps ( ) {
// Get device information
const result = await this . sendCommand ( "PFMSNP" ) ;
this . gpsname = result [ 0 ] ;
// Compute unit id as a XOR of the result
this . gpsunitid = 0 ;
for ( let i = 0 ; i < result . length ; i ++ ) {
for ( let j = 0 ; j < result [ i ] . length ; j ++ ) {
this . gpsunitid ^= result [ i ] . charCodeAt ( j ) << ( ( j % 4 ) * 8 ) ;
try {
// Get device information
const result = await this . sendCommand ( "PFMSNP" ) ;
this . gpsname = result [ 0 ] ;
// 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 ++ ) {
this . gpsunitid ^= result [ i ] . charCodeAt ( j ) << ( ( j % 4 ) * 8 ) ;
}
}
// Download track list
await this . sendSmplCommand ( "PFMDNL" , [ "LST" ] ) ;
this . saved _tracks = [ ] ;
let totaltracks = 0 ;
do {
const trackres = await this . receiveData ( "PFMLST" ) ;
totaltracks = parseInt ( trackres [ 0 ] ) ;
const date = trackres [ 2 ] ;
const start = trackres [ 3 ] ;
const duration = trackres [ 4 ] ;
// Parse date and time
const [ day , month , year ] = date . split ( '.' ) . map ( n => parseInt ( n ) ) ;
const [ hour , minute , second ] = start . split ( ':' ) . map ( n => parseInt ( n ) ) ;
// Create date object (note: month is 0-indexed in JS Date)
const startDate = new Date ( 2000 + year , month - 1 , day , hour , minute , second ) ;
// Parse duration
const [ dHour , dMinute , dSecond ] = duration . split ( ':' ) . map ( n => parseInt ( n ) ) ;
// Calculate end date
const endDate = new Date ( startDate . getTime ( ) +
( dHour * 3600 + dMinute * 60 + dSecond ) * 1000 ) ;
this . saved _tracks . push ( {
startDate : startDate ,
endDate : endDate
} ) ;
} while ( this . saved _tracks . length < totaltracks ) ;
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 ;
}
}
/ * *
* Read a packet from the device
* This matches the C ++ implementation in FlymasterGps : : read _packet ( )
* /
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" ) ;
}
console . debug ( "Received initial bytes:" , this . bytesToHexString ( initialBytes ) ) ;
// Extract packet ID (first 2 bytes)
const packetId = initialBytes [ 0 ] + ( initialBytes [ 1 ] << 8 ) ;
if ( packetId === this . PACKET _END _MARKER ) {
return { end : true } ;
}
// Extract length (3rd byte)
const length = initialBytes [ 2 ] ;
// Calculate total expected packet size: 3 bytes header + data length + 1 byte checksum
const totalExpectedSize = 3 + length + 1 ;
// Create a buffer for the complete packet
let packetBytes = new Uint8Array ( totalExpectedSize ) ;
// Copy initial bytes to the packet buffer
let bytesReceived = Math . min ( initialBytes . length , totalExpectedSize ) ;
packetBytes . set ( initialBytes . slice ( 0 , bytesReceived ) , 0 ) ;
// 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" ) ;
}
if ( ! moreBytes || moreBytes . length === 0 ) {
continue ;
}
console . debug ( "Received more bytes:" , this . bytesToHexString ( moreBytes ) ) ;
// Calculate how many more bytes we need and how many we can copy
const bytesNeeded = totalExpectedSize - bytesReceived ;
const bytesToCopy = Math . min ( moreBytes . length , bytesNeeded ) ;
// Copy the bytes to the packet buffer
packetBytes . set ( moreBytes . slice ( 0 , bytesToCopy ) , bytesReceived ) ;
bytesReceived += bytesToCopy ;
}
console . debug ( "Complete packet assembled:" , this . bytesToHexString ( packetBytes ) ) ;
// Extract data (next 'length' bytes after header)
const data = new Uint8Array ( length ) ;
for ( let i = 0 ; i < length ; i ++ ) {
data [ i ] = packetBytes [ 3 + i ] ;
}
// Extract checksum (last byte after data)
const cksum = packetBytes [ 3 + length ] ;
// Compute checksum
let c _cksum = length ;
for ( let i = 0 ; i < data . length ; i ++ ) {
c _cksum ^= data [ i ] ;
}
if ( c _cksum !== cksum ) {
// Send negative acknowledgment
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 track list
await this . sendSmplCommand ( "PFMDNL" , [ "LST" ] ) ;
this . saved _tracks = [ ] ;
let totaltracks = 0 ;
do {
const trackres = await this . receiveData ( "PFMLST" ) ;
totaltracks = parseInt ( trackres [ 0 ] ) ;
const date = trackres [ 2 ] ;
const start = trackres [ 3 ] ;
const duration = trackres [ 4 ] ;
// Parse date and time
const [ day , month , year ] = date . split ( '.' ) . map ( n => parseInt ( n ) ) ;
const [ hour , minute , second ] = start . split ( ':' ) . map ( n => parseInt ( n ) ) ;
// Create date object (note: month is 0-indexed in JS Date)
const startDate = new Date ( 2000 + year , month - 1 , day , hour , minute , second ) ;
// Parse duration
const [ dHour , dMinute , dSecond ] = duration . split ( ':' ) . map ( n => parseInt ( n ) ) ;
// Calculate end date
const endDate = new Date ( startDate . getTime ( ) +
( dHour * 3600 + dMinute * 60 + dSecond ) * 1000 ) ;
this . saved _tracks . push ( {
startDate : startDate ,
endDate : endDate
} ) ;
} while ( this . saved _tracks . length < totaltracks ) ;
console . log ( ` Initialized GPS: ${ this . gpsname } , found ${ this . saved _tracks . length } tracks ` ) ;
return this . saved _tracks ;
}
/ * *
* Download a single track
* This matches the C ++ implementation in FlymasterGps : : download _strack ( )
* /
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
const startDate = track . startDate ;
const year = startDate . getFullYear ( ) % 100 ;
const month = ( startDate . getMonth ( ) + 1 ) . toString ( ) . padStart ( 2 , '0' ) ;
const day = startDate . getDate ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const hour = startDate . getHours ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const minute = startDate . getMinutes ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const second = startDate . getSeconds ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const timestamp = ` ${ year } ${ month } ${ day } ${ hour } ${ minute } ${ second } ` ;
// Send download command
await this . sendSmplCommand ( "PFMDNL" , [ timestamp ] ) ;
let newTrack = true ;
let basePos = null ;
let pktCount = 0 ;
let pcounter = 0 ;
const expCount = Math . floor ( ( track . endDate - track . startDate ) / 1000 ) ;
while ( true ) {
const packet = await this . readPacket ( ) ;
// Check if end of transmission
if ( packet . end ) {
break ;
}
if ( packet . packetId === this . PACKET _FLIGHT _INFO ) {
// Flight information packet
this . flightInfo = this . parseFlightInfo ( packet . data ) ;
console . debug ( "Received flight info:" , this . flightInfo ) ;
}
else if ( packet . packetId === this . PACKET _KEY _POSITION ) {
// Key position packet
basePos = this . parseKeyPosition ( packet . data ) ;
result . push ( this . makePoint ( basePos , newTrack ) ) ;
newTrack = false ;
}
else if ( packet . packetId === this . PACKET _DELTA _POSITION ) {
// Delta position packet
const deltas = this . parseDeltaPositions ( packet . data ) ;
for ( const delta of deltas ) {
// Apply delta to base position
basePos . fix = delta . fix ;
basePos . latitude += delta . latoff ;
basePos . longitude += delta . lonoff ;
basePos . gpsaltitude += delta . gpsaltoff ;
basePos . baro += delta . baroff ;
basePos . time += delta . timeoff ;
pktCount += delta . timeoff ;
if ( progressCallback && ! ( pcounter ++ % 60 ) ) {
const continueDownload = progressCallback ( pktCount , expCount ) ;
if ( ! continueDownload ) {
// Send negative acknowledgment
await this . writer . write ( new Uint8Array ( [ this . ACK _NEGATIVE ] ) ) ;
throw new Error ( "Download cancelled" ) ;
}
}
result . push ( this . makePoint ( basePos , false ) ) ;
}
}
}
return result ;
} catch ( error ) {
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
} ;
}
/ * *
* Parse delta positions from a packet
* /
parseDeltaPositions ( data ) {
if ( ! data ) {
throw new Error ( "Invalid delta position data" ) ;
}
const deltas = [ ] ;
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 ) {
const delta = {
fix : data [ i ] ,
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 ;
}
/ * *
* Convert a position to a trackpoint
* This matches the C ++ implementation in make _point ( )
* /
makePoint ( pos , newTrack ) {
return {
lat : pos . latitude / 60000.0 ,
lon : - pos . longitude / 60000.0 ,
gpsalt : pos . gpsaltitude ,
baroalt : ( 1 - Math . pow ( Math . abs ( ( pos . baro / 10.0 ) / 1013.25 ) , 0.190284 ) ) * 44307.69 ,
time : pos . time + 946684800 , // Convert from Y2K epoch to Unix epoch
new _trk : newTrack
} ;
}
/ * *
* Generate NMEA command with checksum
* This matches the C ++ implementation in NMEAGps : : gen _command ( )
* /
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 ;
@ -168,6 +485,7 @@ class FlymasterClient {
/ * *
* Send a command and get response
* This matches the C ++ implementation in NMEAGps : : send _command ( )
* /
async sendCommand ( command , parameters = [ ] ) {
await this . sendSmplCommand ( command , parameters ) ;
@ -176,16 +494,19 @@ class FlymasterClient {
/ * *
* Send a command without waiting for response
* This matches the C ++ implementation in NMEAGps : : send _smpl _command ( )
* /
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 ( ) ;
@ -195,20 +516,24 @@ class FlymasterClient {
let result = [ ] ;
let incmd = false ;
let cksum = 0 ;
const startTime = Date . now ( ) ;
while ( Date . now ( ) - startTime < this . DOWN _TIMEOUT ) {
const { value , done } = await this . reader . read ( ) ;
if ( done ) break ;
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 ;
if ( ch === this . XON || ch === this . XOFF ) {
continue ;
}
if ( ch === 0x24 ) { // '$'
incmd = true ;
cksum = 0 ;
@ -217,222 +542,45 @@ class FlymasterClient {
result = [ ] ;
continue ;
}
if ( ! incmd ) continue ;
if ( ch !== 0x2A ) // '*'
if ( ! incmd ) {
continue ;
}
if ( ch !== 0x2A ) { // Not '*'
cksum ^= ch ;
}
if ( ch === 0x2C || ch === 0x2A ) { // ',' or '*'
if ( param . length ) {
if ( ! recv _cmd . length )
if ( ! recv _cmd . length ) {
recv _cmd = param ;
else
} else {
result . push ( param ) ;
}
}
param = "" ;
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 ) ;
// Skip CR, LF
i += 2 ;
if ( cksum _r === cksum && recv _cmd === command )
if ( cksum _r === cksum && recv _cmd === command ) {
return result ;
}
}
continue ;
}
param += String . fromCharCode ( ch ) ;
}
}
throw new Error ( "Timeout waiting for response" ) ;
}
/ * *
* 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 ) ;
if ( packetId === this . PACKET _END _MARKER ) {
return { end : true } ;
}
// 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 ) ;
data . set ( value . slice ( 0 , bytesToCopy ) , bytesRead ) ;
bytesRead += bytesToCopy ;
}
// Read checksum
const { value : cksumByte } = await this . reader . read ( ) ;
const cksum = cksumByte [ 0 ] ;
// Compute checksum
let c _cksum = length ;
for ( let i = 0 ; i < data . length ; i ++ ) {
c _cksum ^= data [ i ] ;
}
if ( c _cksum !== cksum ) {
// Send negative acknowledgment
await this . writer . write ( new Uint8Array ( [ this . ACK _NEGATIVE ] ) ) ;
throw new Error ( "Data checksum error" ) ;
}
return { packetId , data } ;
}
/ * *
* Download a specific track
* /
async downloadTrack ( trackIndex , progressCallback = null ) {
const track = this . saved _tracks [ trackIndex ] ;
const result = [ ] ;
// Format date for command
const startDate = track . startDate ;
const year = startDate . getFullYear ( ) % 100 ;
const month = ( startDate . getMonth ( ) + 1 ) . toString ( ) . padStart ( 2 , '0' ) ;
const day = startDate . getDate ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const hour = startDate . getHours ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const minute = startDate . getMinutes ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const second = startDate . getSeconds ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const timestamp = ` ${ year } ${ month } ${ day } ${ hour } ${ minute } ${ second } ` ;
// Send download command
await this . sendSmplCommand ( "PFMDNL" , [ timestamp ] ) ;
let newTrack = true ;
let basePos = null ;
let pktCount = 0 ;
let pcounter = 0 ;
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 . packetId === this . PACKET _FLIGHT _INFO ) {
// Flight information packet
// We could parse this, but for now we'll just acknowledge it
}
else if ( packet . packetId === this . PACKET _KEY _POSITION ) {
// Key position packet
basePos = this . parseKeyPosition ( packet . data ) ;
result . push ( this . makePoint ( basePos , newTrack ) ) ;
newTrack = false ;
}
else if ( packet . packetId === this . PACKET _DELTA _POSITION ) {
// Delta position packet
const deltas = this . parseDeltaPositions ( packet . data ) ;
for ( const delta of deltas ) {
// Apply delta to base position
basePos . fix = delta . fix ;
basePos . latitude += delta . latoff ;
basePos . longitude += delta . lonoff ;
basePos . gpsaltitude += delta . gpsaltoff ;
basePos . baro += delta . baroff ;
basePos . time += delta . timeoff ;
pktCount += delta . timeoff ;
if ( progressCallback && ! ( pcounter ++ % 60 ) ) {
const continueDownload = progressCallback ( pktCount , expCount ) ;
if ( ! continueDownload ) {
// Send negative acknowledgment
await this . writer . write ( new Uint8Array ( [ this . ACK _NEGATIVE ] ) ) ;
throw new Error ( "Download cancelled" ) ;
}
}
result . push ( this . makePoint ( basePos , false ) ) ;
}
}
// Send positive acknowledgment
await this . writer . write ( new Uint8Array ( [ this . ACK _POSITIVE ] ) ) ;
} catch ( error ) {
console . error ( "Error reading packet:" , error ) ;
break ;
param += String . fromCharCode ( ch ) ;
}
}
return result ;
}
/ * *
* Parse a key position packet
* /
parseKeyPosition ( data ) {
const view = new DataView ( data . buffer ) ;
return {
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 ]
} ;
}
/ * *
* Parse delta positions from a packet
* /
parseDeltaPositions ( data ) {
const deltas = [ ] ;
const DELTA _SIZE = 9 ; // Size of each delta structure
for ( let i = 0 ; i + DELTA _SIZE <= data . length ; i += DELTA _SIZE ) {
deltas . push ( {
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 ]
} ) ;
}
return deltas ;
}
/ * *
* Convert a position to a trackpoint
* /
makePoint ( pos , newTrack ) {
return {
lat : pos . latitude / 60000.0 ,
lon : - pos . longitude / 60000.0 ,
gpsalt : pos . gpsaltitude ,
baroalt : ( 1 - Math . pow ( Math . abs ( ( pos . baro / 10.0 ) / 1013.25 ) , 0.190284 ) ) * 44307.69 ,
time : pos . time + 946684800 , // Convert from Y2K epoch to Unix epoch
new _trk : newTrack
} ;
throw new Error ( ` Timeout waiting for response to ${ command } ` ) ;
}
/ * *
@ -453,62 +601,22 @@ class FlymasterClient {
* Select tracks for download
* /
selectTracks ( indices ) {
this . selected _tracks = indices . sort ( ( a , b ) => b - a ) ; // Sort in descending order
this . selected _tracks = indices ;
}
/ * *
* Download all selected tracks
* /
async downloadSelectedTracks ( progressCallback = null ) {
let allPoints = [ ] ;
for ( const trackIndex of this . selected _tracks ) {
const points = await this . downloadTrack ( trackIndex , progressCallback ) ;
allPoints = allPoints . concat ( points ) ;
}
return allPoints ;
return await this . downloadTracklog ( progressCallback ) ;
}
}
// Example usage:
async function connectToFlymaster ( ) {
const client = new FlymasterClient ( ) ;
try {
// Connect to device
const connected = await client . connect ( ) ;
if ( ! connected ) {
console . error ( "Failed to connect to device" ) ;
return ;
}
// Initialize GPS
await client . initGps ( ) ;
// Get track list
const tracks = client . getTrackList ( ) ;
console . log ( "Available tracks:" , tracks ) ;
// Select tracks to download (e.g., the most recent one)
client . selectTracks ( [ 0 ] ) ;
// 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
} ) ;
console . log ( ` Downloaded ${ points . length } points ` ) ;
// Disconnect
await client . disconnect ( ) ;
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 ;
}
}