FooGolf user guide
The guide for people who own a FooGolf device. It is written to be read by a customer, not by a developer, but the reference sections are deliberately precise enough for a technical user to debug against.
Started 2026-09-15 with the Analytics settings chapter, joined the same day by Extensions and risk. Other chapters will be added as the guide grows; the list below is the intended shape.
- Getting started — to be written
- Finding your way around — the six buttons
- Games and drills — to be written
- Players and handicaps — to be written
- Online play — to be written
- Screen casting — to be written
- Golf simulators (GSPro) — to be written
- Extensions (write your own drills in Lua) — to be written, but read the risks of running an extension first
- Analytics settings — send your shot data to your own server
- Distance calibration — make the predicted roll match what you measure
- Firmware updates — Settings > Device settings
- Troubleshooting — to be written
Finding your way around
The device has six buttons and no touch screen, so it is worth learning what each one does once. Only three of them ever change what is on screen from a menu.
| Button | On the main menu | In a settings list or an information page |
|---|---|---|
| OK | opens the thing you are on | accepts, or opens the row you are on |
| BACK | moves one cell to the left | goes back one page |
| HOME | goes to what you are playing | goes to what you are playing |
| ▲ ▼ ▶ | move around the cells | move up and down the rows |
HOME always takes you back to your game
Wherever you are — a menu, a settings list, an About page, a scorecard — HOME takes you straight back to whatever you were playing, ready to carry on from where you left off. You never have to walk back out the way you came in. (A very small number of pages use every other button for something, so there HOME is simply the way out of that page.)
When you press HOME from the main menu you will see the menu slide across to the cell of the game or drill you are playing before it opens. That is deliberate: it shows you where on the menu your game lives, so you can find it again. Press HOME a second time if you would rather not wait.
Inside a game or drill, HOME does the opposite — it takes you to the menu, landing on that game's own cell.
BACK leaves you where you were
Backing out of a settings list returns you to the exact cell you opened it from, not to the game you are playing. So you can change one setting, come back out, and carry straight on along the row you were looking at.
The main menu is a grid
Cells are arranged in rows — FooGolf games, Drills, Extensions (only when you have installed one), Online, Sim, Settings. Each row begins with its name on the left; move onto the name with BACK. The FooGolf games row's name is the FooGolf ball logo, and pressing OK on it opens About FooGolf.
A cell with a white outline around it is the game or drill you are playing now. A cell with a dashed outline is the one OK would act on. A small yellow pencil on a cell or row means something inside it has been changed from the factory setting; open it and the last row, Customised settings, lists exactly what — and offers Restore defaults to put the whole list back.
You can make the cells bigger or smaller at Settings > Dev > Visual settings > Cells on view (1, 2 or 3 cells across the screen; 2 is the default).
Extensions and risk
Extensions grid row; Settings → Manage extensions. Firmware 0.8.516 and later; called "scripts" until 0.8.548.
Your device can run drills and games written in Lua by other people. You install one by typing its five-character share code, and from then on it is a cell on your Extensions row like any built-in drill. That is genuinely useful — and it means running code you did not write, so it is worth understanding what a extension can reach before you install one.
An extension is not reviewed for you
The FooGolf extension store checks that an extension compiles and that its manifest is sane. That is all. Approval for the public Browse list is a light look by FooGolf at the author's description — not a security audit — and a share code installs any extension at all, listed or not. Treat a share code from a stranger the way you would treat any small program from a stranger.
What an extension can do
- Own the screen, the buttons and the sound while it is the active experience, and read every shot you hit while it is running.
- Send your shots to your analytics endpoint — if you configured one. A extension declares what it claims you are aiming at, and the device forwards the shot. See the warning below.
- Write to a player's trend history for its own drill: the one number per completed round that the Trends graph shows.
- Read your players' names, handicaps and impact ratios, and your device-wide settings (stimp, skill level, and so on).
- Keep its own settings on the device.
What an extension cannot do
- It cannot invent a measurement. Speeds, angles, path and lie all come from the device's own sensors. An extension chooses which shots it reports and what it says they were aimed at; it cannot make up the numbers.
- It cannot send anything anywhere on its own. The only outbound path is the analytics endpoint you typed in, at most one message per shot. An extension cannot open its own connection, reach the internet, or contact its author.
- It cannot touch your player list — no adding, renaming, deleting or activating players.
- It cannot rate you. Only Course play rounds feed handicaps; no scripted drill or game does, so an extension cannot move your handicap today. If that ever changes, a badly-written or careless extension would be able to distort your handicap the same way a badly-written one can distort your analytics — so the advice below is worth keeping either way.
- It cannot brick the device. An extension that crashes ends on an error page and stops; HOME always gets you out, and removing the extension removes it completely.
The realistic risks
Nothing dramatic — the sandbox is tight. What a poorly written or careless extension can do is quietly spoil your own data:
- Unreasonable shots in your analytics. An extension declares the target
itself. One that reports a target you were not actually putting at, or claims
the wrong distance, puts junk rows in your own data set — and you will not
notice until you go looking at it. Every forwarded row carries
"game": "extension:<id>", so you can always tell an extension's rows from a built-in game's, and filter or drop them. - Unreasonable entries in your player records. An extension writes the trend
number for its own drill. A buggy one can fill that drill's history with
nonsense. Controls → Players →
→ Delete... → Delete trend data... clears it. - A drill that simply scores wrongly and tells you you are better (or worse) than you are. There is no way for the device to check an extension's arithmetic.
Updates and default extensions
Firmware 0.8.551 and later.
Every time the device starts (after its audio pack check) it asks the extension store about each extension you have installed and brings your copies up to date: a newer version is downloaded, and an extension its author has taken down is removed. The start-up page shows the progress ("Extensions - Updating 1 of 3") and then the outcome ("2 of 2 extensions updated", "Up to date"). Your settings and trends for an updated extension are kept. Settings > Manage extensions > Check for updates does the same at any time.
FooGolf marks a few extensions as default: every device installs them at that check, and they show as "NAME (default)" on the Remove page and cannot be removed. Everything else in this chapter applies to them too - they run in the same sandbox with the same limits.
Staying safe
- Install extensions from people you have some reason to trust.
- Watch the first round. Does the distance it shows match what it says, and do the scores make sense?
- If you forward shots to your own server, check what a new extension sends
before you keep its rows — filter on
gameand eyeball thetarget.distance_ftvalues. - Remove an extension you do not trust: Settings → Manage extensions → Remove an extension. Its settings and trend history go with it.
- Your own analytics endpoint is under your control: Settings → Analytics settings → Clear IP address stops everything, extensions included.
Analytics settings
Attached devices → Analytics endpoint. Firmware 0.8.525 and later; it was Settings → Analytics settings until 0.8.568.
What it does
Your device can send every shot to a server you run, on your own network, so you can keep your shot data and analyse it however you like — a spreadsheet, a database, a dashboard, a machine-learning experiment, whatever you want.
Nothing is sent until you type in an address. FooGolf does not process this data and the FooGolf cloud is not involved: the device talks straight to your server over your own home network.
Each shot is sent once, as a single JSON HTTP POST, and the device carries on immediately — forwarding never delays your putting.
Turning it on
You need a server on your home network that accepts
POST /foogolf/shot and answers with a 2xx status code. There is a
ready-to-run example at the end of this chapter.
- Make sure the device is connected to WiFi (Online → WiFi).
- Go to Attached devices → Analytics endpoint.
- IP address — type your server's address on your network (for example
192.168.1.50). As soon as you commit the address the device runs a connection test and shows you the result. - Port — the port your server listens on. It starts at
8080, which is the port the example server uses. - Connection test — sends a single test message. You are looking for "Endpoint reached!".
- Status — opens a page showing the endpoint, whether the last send worked, and how many shots have been sent, failed or dropped.
- Clear IP address — forgets the address and turns forwarding off again. The port is kept, so switching back on is one step.
Which shots are sent
Only shots hit at a known target, because analytics about a shot with no target would not mean anything. That is a decision each game and drill makes for itself, and it is deliberately conservative:
| Experience | Sent? | Target reported |
|---|---|---|
| Course play (stroke play / match play / skins) | Yes | the hole, at the current distance to it |
| Close-in putting | Yes | the hole, at the current distance to it |
| Lag putting | Yes | the station, at its distance |
| Metrics only | No | there is no station to aim at |
| Make distance | No | the aim is "as far as possible", not a distance |
| Arc strength consistency | No | nothing is aimed at |
| Face rotation consistency | No | nothing is aimed at |
| Face angle | No | nothing is aimed at |
| Swing path and lie | No | nothing is aimed at |
| Mini golf | No | the ball banks off walls, so the shot is not a straight putt at the target |
| Extensions (Lua drills and games) | If the extension says so | whatever target the extension declares — read Extensions and risk |
| GSPro / GSPro connector / Sim sharing | No | the simulator owns the shot |
In an online round a device only reports its own players' putts. A putt the device is going to ignore because it is another device's turn is never reported.
The message
One POST to http://<your address>:<your port>/foogolf/shot, with
Content-Type: application/json. A real shot looks like this (formatted here
for readability; the device sends it on one line):
{
"schema": "foogolf.shot.v1",
"type": "shot",
"device_id": "3f2b91c4-...",
"device_name": "Ferocious ferret",
"firmware": "0.8.525",
"time": 1757913600,
"uptime_ms": 412345,
"game": "lag",
"game_name": "Lag putting",
"player_id": 3,
"player_name": "Andrew",
"target": { "kind": "station", "distance_ft": 26.00, "hole": 3, "stroke": 1 },
"shot": {
"club_speed_mps": 1.830,
"ball_speed_mps": 3.020,
"impact_ratio": 1.650,
"face_angle_deg": -0.60,
"face_rotation_deg_per_in": 0.420,
"attack_angle_deg": 1.10,
"left_inner_proportion": 0.503,
"right_inner_proportion": 0.497
},
"stimp": 10
}
| Field | Meaning |
|---|---|
schema |
always foogolf.shot.v1. Check it, and ignore messages you do not recognise. |
type |
shot for a real shot, test for the Connection test message. |
device_id |
the device's permanent id. Stable for the life of the device. |
device_name |
the name shown on the device's status page. May be empty on a brand-new device. |
firmware |
firmware version that produced the message. |
time |
UNIX epoch seconds, or 0 if the device's clock has not synced yet. Use your own server clock if this is 0. |
uptime_ms |
milliseconds since the device booted. Always present, so it is the reliable way to order shots within a session. |
game |
stable identifier of the game or drill: golf_course, close, lag. An extension's rows read extension:<its share code> (firmware 0.8.553 and later; the manifest id before that), so they are always distinguishable from a built-in game's. (It was script:<its id> on firmware before 0.8.549.) New ones may appear; do not assume the list is closed. |
game_name |
the human name, for display. |
player_id |
the player's id in the device's own player list. Stable until that player is deleted. |
player_name |
that player's name. |
target.kind |
hole or station. |
target.distance_ft |
straight-line distance from the ball to the target, in feet, at the moment of the shot. |
target.hole |
hole or station number, 1-based. 0 means not applicable. |
target.stroke |
which stroke this was at that target, 1-based. 0 means not applicable. |
shot.club_speed_mps |
measured putter head speed at impact, metres per second. |
shot.ball_speed_mps |
club_speed_mps × impact_ratio. |
shot.impact_ratio |
the ball/club speed ratio the device used for this player. |
shot.face_angle_deg |
face angle at the ball. Negative = closed/left, positive = open/right. |
shot.face_rotation_deg_per_in |
face rotation through the impact gate. Positive = opening. |
shot.attack_angle_deg |
angle of attack. |
shot.left_inner_proportion, shot.right_inner_proportion |
the raw sensor proportions behind the path-and-lie readout. |
stimp |
the green speed the shot was judged on. |
x_factor |
the X factor in force (Settings → Device settings → Game settings → X factor, calibrated at Settings → Device settings → Distance calibration; 1.00 unless you changed it). The device's predicted roll is its model × this. Firmware 0.8.552 and later; absent before. |
The Connection test sends the same envelope with "type": "test" and none of
the game / player / target / shot fields.
Rules and restrictions
These are enforced in the device, both when you save the setting and again immediately before every single send. If a message is not arriving, work through this list first — it is almost always one of these.
Address
- Only a literal IPv4 address and a port. Hostnames are not accepted — there is nowhere to type one, and DNS is deliberately not used.
- Only private (RFC1918) addresses:
10.0.0.0/8—10.x.x.x172.16.0.0/12—172.16.x.xto172.31.x.x192.168.0.0/16—192.168.x.x- Port must be
1–65535. - Everything else is refused with an error on screen. The device never quietly clamps a value or substitutes a different one: if the address is refused, the old setting is still in force. Explicitly refused:
0.0.0.0/8(unspecified)127.0.0.0/8(loopback — that is the device, not your PC)169.254.0.0/16(link-local; means DHCP failed)224.0.0.0/4(multicast)255.255.255.255(broadcast)- any public address
- If a previously-saved address stops being allowed (after a firmware update that tightens the rules, say), it is not used and forwarding stays off.
HTTP
- Plain HTTP only. Never HTTPS. Do not put a TLS-only reverse proxy in front of the endpoint.
- Redirects are never followed. Any
3xxresponse counts as a failed send. Serve the endpoint directly at the path — no301from/to/foogolf/shot, no trailing-slash redirect, no HTTP→HTTPS redirect. This is the single most common cause of "the test says it failed but my server log shows the request arrived". - The response body is ignored entirely. Only the status code is read, and
it must be
2xx. You cannot send anything back to the device. - Connect timeout ~2 s, total request timeout ~2 s. A server that takes
longer than that to answer will look unreachable. Answer
200first and do your work afterwards. - No keepalive: each shot opens and closes its own connection.
Rate and failures
- Exactly one POST per shot. There is no polling, no heartbeat, no reconnect loop. If your server sees repeated traffic, it is not this feature.
- A send that fails is retried at most twice, after 1 s and then 3 s, and is then dropped. There is no long-term store-and-forward: a shot missed while your server was down is gone.
- The queue holds 4 shots. If it fills up (server slow or down while you keep putting), the oldest queued shot is dropped.
- Failures are silent on the device except for the status: Settings →
Analytics endpoint → Status shows
Last send failed, with the HTTP code when there was one, and the sent / failed / dropped counts. Settings → Controls → Device status carries the same line. - Sending runs on its own background task. It can never delay shot detection, the screen, or the sound.
Troubleshooting
| What you see | Almost always means |
|---|---|
| The address is refused when you commit it | It is outside 10.x / 172.16-31.x / 192.168.x. The error page names the rule. |
Cannot connect to <address>:<port> |
Nothing is listening there. Wrong IP, wrong port, server not started, server bound to 127.0.0.1 instead of 0.0.0.0, or a firewall on the server machine. |
| Connection test fails but your server log shows the request | Your server answered a 3xx or a non-2xx code. Redirects are never followed. Answer 200 on POST /foogolf/shot. |
... answered HTTP 404 |
The path is wrong. It must be exactly /foogolf/shot. |
... answered HTTP 405 |
Your route only accepts GET. Accept POST. |
... answered HTTP 500 |
Your handler threw. Answer 200 before you parse, then parse. |
| Test passes, but no shots arrive | You are in an experience that does not report (see the table above). Try Lag putting or Course play. |
| Some shots arrive, some do not | Your server is slower than the ~2 s timeout, or it was briefly unreachable and those shots were dropped after their two retries. Check the dropped count on the Status page. |
| Nothing arrives after the device's IP changed | Nothing to do with the device: give your server a fixed address (a DHCP reservation on your router). If your server's IP moves, the setting stops matching. |
| Everything worked yesterday and not today | Same cause as above nine times out of ten. Check the address on the Status page against your server's current address. |
Status says Off |
No address is set (or a saved one is no longer allowed). Re-enter it. |
An example server
Dependency-free — Python 3 only. It answers 200 immediately and appends each
shot to a CSV, which is enough to start analysing in a spreadsheet.
#!/usr/bin/env python3
"""Minimal FooGolf analytics endpoint. Run on your LAN: python3 foogolf_sink.py"""
import csv, json, os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
PORT = 8080
CSV_PATH = "foogolf_shots.csv"
FIELDS = ["time", "uptime_ms", "device_name", "game", "player_name",
"target_kind", "distance_ft", "hole", "stroke",
"club_speed_mps", "ball_speed_mps", "face_angle_deg",
"face_rotation_deg_per_in", "attack_angle_deg", "stimp"]
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1" # keeps the 2 s budget comfortable
def do_POST(self):
if self.path != "/foogolf/shot":
self.send_error(404)
return
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
# Answer FIRST: the device gives up after about two seconds.
self.send_response(200)
self.send_header("Content-Length", "0")
self.end_headers()
try:
msg = json.loads(body)
except ValueError:
return
if msg.get("schema") != "foogolf.shot.v1":
return
if msg.get("type") != "shot":
print("test message from", msg.get("device_name"))
return
t, s = msg.get("target", {}), msg.get("shot", {})
row = {
"time": msg.get("time"), "uptime_ms": msg.get("uptime_ms"),
"device_name": msg.get("device_name"), "game": msg.get("game"),
"player_name": msg.get("player_name"),
"target_kind": t.get("kind"), "distance_ft": t.get("distance_ft"),
"hole": t.get("hole"), "stroke": t.get("stroke"),
"club_speed_mps": s.get("club_speed_mps"),
"ball_speed_mps": s.get("ball_speed_mps"),
"face_angle_deg": s.get("face_angle_deg"),
"face_rotation_deg_per_in": s.get("face_rotation_deg_per_in"),
"attack_angle_deg": s.get("attack_angle_deg"),
"stimp": msg.get("stimp"),
}
new = not os.path.exists(CSV_PATH)
with open(CSV_PATH, "a", newline="") as f:
w = csv.DictWriter(f, FIELDS)
if new:
w.writeheader()
w.writerow(row)
print(row)
def log_message(self, *a):
pass # keep stdout to the shots
if __name__ == "__main__":
# 0.0.0.0, not 127.0.0.1 - the device is a different machine.
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
Run it, note the machine's LAN address (ipconfig on Windows, ip addr on
Linux, ifconfig on a Mac), and enter that address and 8080 on the device.
Notes and limits
-
Extensions (Lua drills and games) can forward shots since firmware 0.8.530, but only if the extension declares a target. The extension decides which shots and what they were aimed at, so read Extensions and risk before you keep a new extension's rows.
-
Shots with no target are never forwarded, by design. If you want raw per-shot measurements with no target at all, that is a different feature and it does not exist yet.
- There is no store-and-forward. If your server is down, those shots are lost. Run the server, then putt.
Distance calibration
Settings → Device settings → Distance calibration. Firmware 0.8.552 and later; the page below is firmware 0.8.564 and later.
What it does
Every game and drill that rolls a ball predicts how far your putt travels from three things: the club head speed the device measured, your impact ratio (ball speed divided by club head speed, a property of you and your putter) and the stimp you set. Real surfaces rarely match a model exactly, and you may not have a stimp meter at all. Distance calibration lets you measure one putt and have the device correct itself.
There are two corrections, and they suit different situations:
| X factor | Impact ratio | |
|---|---|---|
| What it is | one multiplier on every predicted distance (1.00 = the model as it is) |
the ball/club speed ratio of one player's putter |
| Who it affects | every player on the device | that player only |
| Use it when | the surface is what differs: everyone's putts are off by the same proportion | one player's putter is what differs |
| Range | 0.50 to 2.00 | 1.40 to 1.80 |
A sensible order: set the X factor first using a player whose ratio you trust, then adjust the other players' ratios one at a time.
How to use it
- Hit a putt in any game or drill. Measure how far the ball actually rolled, in feet.
- Go to Settings → Device settings → Distance calibration → Calibrate from last shot. The page is headed Calibrate from last shot and shows, for that last putt: the Player and the Impact ratio and Stimp the prediction used, the measured Club speed, the X factor in force and the Predicted distance. These are there to be read — there is nothing to press on them.
- Underneath is a menu of three choices. Adjust X factor and Adjust impact ratio are greyed out and the cursor skips them until you have entered a measurement: you can see they are coming, but they have nothing to work with yet.
- Enter measured distance — type the distance you measured on the number pad (digits, one decimal point, Cancel, OK). Feet, 0.1 to 300. You come straight back to the page, and the other two choices light up.
- Choose a correction:
- Adjust X factor shows the factor that would have made the
prediction equal your measurement, next to the current one. OK
applies it, BACK keeps the old value. Either way you land back on
this page.
- Adjust impact ratio for
<name>shows the ratio that player would need, keeping the current X factor. OK changes only that player, BACK keeps their old ratio. - Hit another putt to check. A new putt replaces the last shot and clears the measured distance — so the two Adjust choices grey out again, even while you are looking at the page.
You can also type the X factor directly, at Settings → Game settings → X factor. The device never rounds or clamps a value you enter: anything outside the range is refused with a message and nothing changes. If a measurement would need a value outside the range, the page tells you what it would have needed and suggests the other correction — check the measurement and the stimp first.
About is a row of its own in Settings → Device settings → Distance calibration, next to the one that opens this page.
Things to know
- The X factor is a device setting: it persists across restarts, counts as a customised setting (the yellow edit pencil on the Game settings row, where Restore defaults puts it back to 1.00), and while you host an online round your guests adopt it so everyone sees the same roll.
- The impact ratio is stored on the player record, exactly as if you had edited it under Players.
- Analytics messages carry the X factor as
x_factor(see the Analytics chapter), so a server that recomputes distances has everything it needs. - Calibrate on a flat run of typical length. A single putt is a single sample: if a second measurement disagrees, split the difference rather than chasing each one.
Firmware updates
Your device updates itself. When it starts up, and whenever it joins your Wi-Fi, it asks FooGolf whether there is a newer version. If there is, it installs it, restarts once, and carries on — you are not asked, and there is nothing to do.
You can change that at Settings > Device settings > Over-the-air updates:
| Always update | The default. A new version installs by itself at start-up or when the device joins Wi-Fi. The screen shows INSTALLING UPDATE with a progress bar, then the device restarts. It takes a couple of minutes. |
| Ask player | The device shows you the new version and waits. Press OK to install it, BACK to leave it for now. It will offer again next time it starts. |
| Disabled | New versions are never installed. |
Choose Ask player if you do not want a restart in the middle of a session — for example if the device is set up for a group and you would rather update it afterwards.
The device never checks on a timer. It checks when it starts up, when it joins Wi-Fi, and nowhere else, so an update can never interrupt play.
If a release will not install
Occasionally a release fails — a weak Wi-Fi signal during the download, or a version that does not start up properly and is automatically rolled back to the one you had. The device tries three times and then leaves that version alone. It stays completely usable, keeps checking, and installs the next release normally. You will never find it restarting over and over.
Settings > Device status shows what happened:
Auto update: Always update
v0.8.556 blocked after 3 tries
If you want it to try that version again, open Settings > Device settings > Over-the-air updates and pick a mode (re-picking the one you are already on is enough) — that starts the three tries over.
Other things on this page
- Over-the-air updates counts as a customised setting when it is not on Always update: the Device settings row gets the yellow edit pencil, and Customised settings > Restore defaults puts it back.
- Extensions update separately, at start-up, and are covered in Updates and default extensions.