commit
						a932b425dc
					
				@ -0,0 +1,520 @@ | 
				
			||||
// flymaster-client.js
 | 
				
			||||
// WebSerial implementation of the Flymaster GPS protocol
 | 
				
			||||
 | 
				
			||||
class FlymasterClient { | 
				
			||||
  constructor() { | 
				
			||||
    // Constants
 | 
				
			||||
    this.BAUDRATE = 57600; | 
				
			||||
    this.XON = 0x11; | 
				
			||||
    this.XOFF = 0x13; | 
				
			||||
    this.MAX_LINE = 90; | 
				
			||||
    this.DOWN_TIMEOUT = 1000; // 1 second in milliseconds
 | 
				
			||||
    
 | 
				
			||||
    // 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 = []; | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /** | 
				
			||||
   * Request port access and open connection | 
				
			||||
   */ | 
				
			||||
  async connect() { | 
				
			||||
    try { | 
				
			||||
      // Request a port and open it
 | 
				
			||||
      this.port = await navigator.serial.requestPort(); | 
				
			||||
      await this.port.open({ 
 | 
				
			||||
        baudRate: this.BAUDRATE, | 
				
			||||
        dataBits: 8, | 
				
			||||
        stopBits: 1, | 
				
			||||
        parity: "none", | 
				
			||||
        flowControl: "none" // We'll handle XON/XOFF in software
 | 
				
			||||
      }); | 
				
			||||
      
 | 
				
			||||
      // 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) { | 
				
			||||
      console.error("Connection failed:", error); | 
				
			||||
      return false; | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /** | 
				
			||||
   * Close the connection | 
				
			||||
   */ | 
				
			||||
  async disconnect() { | 
				
			||||
    if (this.reader) { | 
				
			||||
      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 | 
				
			||||
   */ | 
				
			||||
  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); | 
				
			||||
      } | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    // 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; | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /** | 
				
			||||
   * 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 | 
				
			||||
   */ | 
				
			||||
  async sendSmplCommand(command, parameters = []) { | 
				
			||||
    const cmdStr = this.genCommand(command, parameters); | 
				
			||||
    const encoder = new TextEncoder(); | 
				
			||||
    const data = encoder.encode(cmdStr); | 
				
			||||
    await this.writer.write(data); | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /** | 
				
			||||
   * 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(); | 
				
			||||
    
 | 
				
			||||
    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; | 
				
			||||
        
 | 
				
			||||
        if (ch === 0x24) { // '$'
 | 
				
			||||
          incmd = true; | 
				
			||||
          cksum = 0; | 
				
			||||
          param = ""; | 
				
			||||
          recv_cmd = ""; | 
				
			||||
          result = []; | 
				
			||||
          continue; | 
				
			||||
        } | 
				
			||||
        
 | 
				
			||||
        if (!incmd) continue; | 
				
			||||
        
 | 
				
			||||
        if (ch !== 0x2A) // '*'
 | 
				
			||||
          cksum ^= ch; | 
				
			||||
        
 | 
				
			||||
        if (ch === 0x2C || ch === 0x2A) { // ',' or '*'
 | 
				
			||||
          if (param.length) { | 
				
			||||
            if (!recv_cmd.length) | 
				
			||||
              recv_cmd = param; | 
				
			||||
            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) | 
				
			||||
              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; | 
				
			||||
      } | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    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 | 
				
			||||
    }; | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /** | 
				
			||||
   * 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) | 
				
			||||
      }; | 
				
			||||
    }); | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /** | 
				
			||||
   * Select tracks for download | 
				
			||||
   */ | 
				
			||||
  selectTracks(indices) { | 
				
			||||
    this.selected_tracks = indices.sort((a, b) => b - a); // Sort in descending order
 | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  /** | 
				
			||||
   * 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; | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// 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(); | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// Export the class
 | 
				
			||||
if (typeof module !== 'undefined' && module.exports) { | 
				
			||||
  module.exports = { FlymasterClient }; | 
				
			||||
} else { | 
				
			||||
  window.FlymasterClient = FlymasterClient; | 
				
			||||
} | 
				
			||||
					Loading…
					
					
				
		Reference in new issue