This project is not affiliated with or endorsed by FlightScope (Pty) Ltd. FlightScope and Mevo are trademarks of their respective owner.
Client library for the FlightScope Mevo+ / Mevo Gen2 / Xi protocol. Two
non-blocking clients share the same poll() pattern:
BinaryClient— binary radar protocol (TCP port 5100). Handles handshake, configuration, arming, keepalives, and shot detection.GvpClient— GVP camera protocol (TCP port 1258,gvpfeature). Handles trigger, trajectory hints, and ball tracker results.driver::Mevo— a device driver for the Flight Relay Protocol SDK's driver contract (driverfeature): connect, configure, arm, reconnect and report shots as FRP events. Theironsight-frpbinary (frpfeature) serves it to FRP controllers over WebSocket.
Both clients are generic over any Read + Write stream. Blocking convenience
wrappers and low-level sequencer state machines are also available. No async
runtime or heavyweight dependencies.
This project is an exercise of the interoperability exception under 17 U.S.C. § 1201(f):
(f) Reverse Engineering.—
(1) Notwithstanding the provisions of subsection (a)(1)(A), a person who has lawfully obtained the right to use a copy of a computer program may circumvent a technological measure that effectively controls access to a particular portion of that program for the sole purpose of identifying and analyzing those elements of the program that are necessary to achieve interoperability of an independently created computer program with other programs, and that have not previously been readily made available to the person engaging in the circumvention, to the extent any such acts of identification and analysis do not constitute infringement under this title.
(2) Notwithstanding the provisions of subsections (a)(2) and (b), a person may develop and employ technological means to circumvent a technological measure, or to circumvent protection afforded by a technological measure, in order to enable the identification and analysis described in paragraph (1), or for the purpose of enabling interoperability of an independently created computer program with other programs, if such means are necessary to achieve such interoperability, to the extent that doing so does not constitute infringement under this title.
The FlightScope Mevo+ uses a proprietary binary protocol over TCP to communicate shot data (ball speed, launch angle, spin, club data, etc.) to companion software. FlightScope does not publish this protocol or provide an SDK for third-party integration. The protocol was worked out from WiFi traffic between the researcher's own lawfully purchased device and the official FS Golf app, and from analysis of that app, solely to enable interoperability with third-party golf simulation software.
No FlightScope code is reproduced here; the protocol is described and implemented in our own words and names. No access controls were circumvented — the device operates as an open WiFi access point, and all traffic was captured from the researcher's own hardware.
This project exists solely to enable interoperability between the FlightScope Mevo+ / Mevo Gen2 and third-party golf simulation software.
It must not be used to:
- Circumvent licensing or subscription requirements on FlightScope products
- Unlock paid features (ProPackage, Fusion Tracking, etc.) without purchase
- Bypass any access controls on FlightScope software or services
The sensor activation exchange (0x90/0x89) is documented only to the extent
needed for device communication. The encrypted licensing certificate is opaque
to this library and is not decoded, forged, or tampered with.
Issues or discussions proposing circumvention of licensing will be closed and the user blocked.
Licensed under either of:
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
at your option.
Pre-1.0 (0.x). The protocol is reverse-engineered and still being mapped —
some message types and modes aren't decoded yet, and the API may change as
that work continues. This crate follows SemVer: while
the version is 0.x, any minor release may contain breaking changes. Once the
API stabilizes it will move to 1.0.
| Device | SSID | Status |
|---|---|---|
| Mevo+ (Gen1) | FS M2-XXXXXX |
Fully supported |
| Mevo Gen2 | FS MG2-XXXXXX |
Fully supported |
| Xi | — | Supported |
The model family (DeviceModel) is detected during the handshake from the DSP
device info (0xE7): the Xi names itself FS Xi there. Mevo generation is
detected from the 0xC8 DSP query response (dspType: 0x80 = Mevo+,
0xC0 = Gen2).
The Xi speaks the same protocol on the same address and port
(192.168.2.1:5100), with the same framing and bus addresses. It has no camera
Pi, so BinaryClient adapts once the handshake has identified it:
- The PI handshake phase is skipped;
HandshakeOutcome::piisNone. - Nothing is ever sent to the PI bus address: no PI status polls in arming or
keepalives, and
configure_cam()sends nothing (it reportsConfiguredstraight away). - ironsight does not rely on the DSP query (
0x48): the Xi's DSP may not answer it, and without an answer the handshake moves on afterDSP_QUERY_TIMEOUT(2 s), leavingDspSync::hw_infoasNone. - The DSP sync ends with a power-mode write to the DSP,
BF [06 00 00 01 00 00 01](DSP param0x01= 1, Normal; 4 is Standby), answered by0x95 [02 40 3F]. It wakes a DSP that was left in Standby, and changes nothing on one already in Normal. With no answer withinDSP_QUERY_TIMEOUT(2 s) the handshake carries on and setsDspSync::power_mode_unanswered. - The AVR sync stops after the IF calibration exchange (no factory
calibration, hardware config query, param
0x64or time sync). For a few seconds after power-on the Xi's AVR refuses status polls (0x94 [02 30 2A]); the handshake re-sends the poll everySTATUS_REPOLL_INTERVAL(1 s) until the AVR answers. This applies to every model, and changes nothing when the AVR answers the first poll. BinaryClient::configure_xi(XiSettings)sends the Xi its full parameter set, radar calibration and mode on the connection's first configuration (the connect-time block, with the radar calibration fromXiSettings::radar_cal, followed by the mode block), then the mode block alone on a later call, disarming first if the Xi is armed. The mode block carries the detection mode and the shot type. Build the settings withXiSettings::full_swing(mode, radar_cal), inMODE_LIMITED_FLIGHT(9) for nets and simulator screens orMODE_FULL_OUTDOOR(8), orXiSettings::pitching(radar_cal)for chips and pitches, which always runs in mode 9.XiSettings::default()is full swing in mode 9 with 2438 mm to the ball and a 25 mm surface height. Each frame waits for its reply. A0x94refusing a frame (or the DSP power-mode write) ends that step too:BinaryClientreports it asBinaryEvent::StepRefused, carrying the0x94frame, and carries on.AvrSettings::xi()andAvrSettings::xi_with_mode(mode)are deprecated: they send the detection mode only, with no parameter writes or radar calibration.- Shot results (
E8,D4,ED,EF) decode with the Mevo+ layouts.BinaryClientacks everyE8andD4with0x95the moment it arrives ([02 10 68]/[02 10 54]);seq::result_ackbuilds the ack for callers that drive the sequencers themselves.
It connects, arms and reports shots on a real Xi. Full swing and pitching in mode 9 are tested; mode 8 sends the same frames with only the mode changed, and is untested. ironsight has no putting configuration for the Xi. See SEQUENCE.md.
seq::MevoSettings holds a full configuration: detection mode, ball type
(param 0x06), minimum track percentage as a fraction (written to param
0x0F, whose exact use by the device is not yet confirmed), tee height in
meters (param 0x26) and the radar calibration. Build it with
MevoSettings::new. It converts into
the AvrSettings that BinaryClient::configure_avr takes. For a mode change,
send AvrSettings::mode_only(mode): the device re-arms sooner after the mode
alone than after a full parameter set.
Send CamConfig::warmup_preset() when the session starts. The camera accepts
a Fusion configuration (CamConfig::fusion_preset(),
CamConfig::raw_fusion_preset()) only after it has run in standard mode for
camera::WARMUP (15 s). A camera configuration the device refuses (0x94) or
does not finish within client::CAMERA_SETUP_TIMEOUT (8 s) ends with
BinaryEvent::CameraSetupUnfinished, which names the step it stopped at; the
session goes on with the next queued operation (the arm), and the camera stays
as the device has it.
Shot results report directions as the device measures them: azimuth, lateral
distances, sidespin, club path, face angle and swing plane direction are
positive to the left of the target line and negative to the right. Positive
sidespin curves the ball left. The usual convention in golf software (and in
FRP) is positive to the right, so negate these when converting; the driver's
conversions (driver::convert) do. See the protocol::shot docs.
Detailed specs live in docs/:
- WIRE.md — Frame format, byte stuffing, checksum, field encodings, and the complete message catalog (43 decoded message types).
- SEQUENCE.md — Connection lifecycle, six handshake phases with timing, shot-to-shot message flow, re-arm logic.
- CAMERA.md — Port 1258 JSON protocol (GVP camera API), port 8080 MJPEG stream, per-shot video pipeline.
See examples/client.rs for a complete standalone
example: connect, handshake, configure, arm, and poll for shots.
BinaryClient wraps the connection and all protocol state machines behind a
single poll() call. Keepalives, operation sequencing, and shot detection are
handled automatically:
use ironsight::client::{BinaryClient, BinaryEvent};
use ironsight::protocol::handshake::DeviceModel;
use ironsight::seq::XiSettings;
use ironsight::BinaryConnection;
let conn = BinaryConnection::connect("192.168.2.1:5100")?;
let mut client = BinaryClient::from_tcp(conn)?;
client.handshake();
loop {
match client.poll()? {
Some(BinaryEvent::Handshake(h)) => {
println!("Connected to {}", h.model);
if h.model == DeviceModel::Xi {
client.configure_xi(XiSettings::default());
} else {
client.configure_avr(avr_settings.clone());
client.configure_cam(cam_config.clone());
}
client.arm();
}
Some(BinaryEvent::Armed) => println!("Ready for shots"),
Some(BinaryEvent::ShotComplete(data)) => println!("Shot! {data:?}"),
_ => {}
}
}For callers that need full control over the event loop, the Sequence trait
exposes the underlying pollable state machines. Each sequencer has feed()
(accept a message, return commands to send) and is_complete(). The drive()
function runs any sequencer to completion on a blocking stream. See
examples/event_loop.rs.
Three features build on the flightrelay SDK (0.5, FRP 0.2.0):
config:ironsight::config::Config, the driver's settings as a serde struct (flightrelay'sDriverConfig), withvalidate()and the mapping toMevoSettings/XiSettings. It needs no device stack and builds forwasm32-unknown-unknown, so a settings form can use it.driver:ironsight::driver::Mevo, flightrelay'sDeviceDriverfor the Mevo+, Gen2 and Xi, anddriver::convert(shot results to FRP types).frp: theironsight-frpbinary.
The driver configures the device for the detected model in the detection mode
the host has latched, and re-arms it when a controller's
set_detection_mode changes what is sent to the device (on the Xi, chipping
and putting are one session). Full swing runs in mode 9 (limited flight) or 8
(environment = "outdoor"); on the Xi, chipping and putting use the pitching
shot type. The radar has no club or handedness setting.
What it sends:
- Identity: manufacturer
FlightScope, modelMevo+,Mevo Gen2orXi, firmware from the AVR device info (version and build date). The FRPdeviceid is the Mevo's WiFi SSID, orFlightScope Xi (<address>)(FlightScope Mevo (<address>)for a Mevo that reports no SSID). Nolabelis set, so the card text is manufacturer and model (FlightScope Xi). - Telemetry:
ready,tilt,roll, and on the Mevo+battery_pct,temp_candexternal_power(the Gen2's and Xi's status layout has no decoded values for them, so they are left out). - Shots:
shot_triggerwith a timestamp, thenlaunchandflight_result(basis: modeled: carry, offline, apex, flight time, descent, and total and roll when the device reports them) as soon as the full result (D4) arrives,club_pathonce the shot has a ball, andshot_finishedwhen the device is done. Each goes out once.- A launch-only full result (no spin and no flight, launched at 5° or more,
as the Xi sends for very short chips) sends a
launchwithout spin and noflight_result. Below 5° the same result is a ball that stayed on the ground, and goes out as measured (apex 0). - Without a full result, the early result (
E8) makes the shot whenuse_estimated = true; a shot the device did not read raises an alert and sends no ball. - Xi only,
spin_source:d4(the default, the spin FS Golf displays),e8(the early result's total spin, split by the full result's axis when the shot has club data, otherwise sent alone with no axis) orauto(e8in full swing). It applies without a new session.
- A launch-only full result (no spin and no flight, launched at 5° or more,
as the Xi sends for very short chips) sends a
- Raw frames when a controller asks for them, once the handshake is
done: one message each, the payload as
dataand"<type> <src>><dst>"aslabel(e.g."D4 AVR>APP"). The handshake's messages, those that can identify the unit, its network or the host (protocol::carries_identity), and the device's free text (0xE3) and DSP debug output (0xF0) are never sent; they go to the host's log as hex trace lines.
A message the driver cannot decode raises an alert naming the error (never the message's bytes, which go to the trace log) and the session goes on. A closed connection, an operation timeout or 10 s without a message ends the session, and the host reconnects (1 s, growing by 1 s per failed attempt to 15 s, back to 1 s after a session that lasted 10 s).
ironsight-frp [--config <path>] [--target <listen-addr | ws://host:port/frp>]
[--set <key>=<value>]... [--log <info|debug|trace>] [--help]
--config: a TOML file with the settings below at the top level.--set key=value: one setting, over the file; repeatable.--target: an address to listen on for controllers (default0.0.0.0:5880; several controllers at once), or aws://URL of a controller to dial, such as flighthook.--log: the lowest level of the driver's log lines on stderr (defaultinfo).--help: usage and every setting with its default.
| Key | Default | |
|---|---|---|
address |
192.168.2.1:5100 |
the device's IP address and port |
environment |
limited_flight |
or outdoor (full flight, about 40 yd needed) |
range |
8ft |
distance from the device to the ball |
surface_height |
0in |
height of the hitting surface |
tee_height |
1.5in |
Mevo+ and Gen2 |
ball_type |
1 |
Mevo+ and Gen2: 0 = standard, 1 = RCT |
track_pct |
80 |
Mevo+ and Gen2, 0 to 100 |
camera_mode |
standard |
Mevo+ and Gen2: fusion or raw_fusion (Pro Package); a Fusion setup the device doesn't finish raises an alert and the camera stays in standard mode |
use_estimated |
false |
send a shot from the early result when the full one is missing |
spin_source |
d4 |
Xi: e8 or auto |
# Mevo+ at the default address, serve controllers on port 5880
cargo run --features frp --bin ironsight-frp
# Another address, bridged to flighthook
cargo run --features frp --bin ironsight-frp -- \
--set address=10.0.0.20:5100 --target ws://flighthook:5880/frpPre-built binaries for Linux and Windows are attached to each GitHub release.
The testing feature (off by default) adds ironsight::testing for the
tests of applications built on ironsight:
FakeDevice: a scripted in-memory device (Read + Write) to hand toBinaryConnection::new. It answers status polls, the arm and the shot-completion requests itself;FakeDevice::xi()also answers the Xi's handshake and configuration, andFakeDevice::mevo_plus()/FakeDevice::mevo_gen2()the Mevo handshake, AVR and camera configuration (with_ssidsets the camera Pi's WiFi name,set_dsp_statusthe status it reports).Shot: one shot's frames (Shot::xi_full_swing(),Shot::xi_chip(),Shot::xi_no_read(), or your own results withShot::new), queued withFakeDevice::push_shot.fixtures: result frames as an Xi sent them, none of which identifies a unit.- Payload encoders for test frames:
avr_status,dev_info,shot_text,mevo_plus_dsp_status. - With the
driverfeature,driver::Mevo::connect_withruns a whole driver session (handshake, configuration, shots) on any of these fakes.
[dev-dependencies]
ironsight = { version = "0.5.1", features = ["testing"] }Minimal by design:
thiserror— error enum derivesserde(optional,serdefeature) — serialization supportserde_json(optional,gvpfeature) — camera protocol supportflightrelay(optional,config,driverandfrpfeatures) — FRP driver contract, settings and server
No async runtime. No logging framework. Standard library TCP only.