Appearance
Theseus RSSI recorder
Added: 11 October 2026
The RSSI recorder is a tool on the start screen. A tester picks one or more watches, starts a recording, and tells Theseus which zone each watch is in while walking the site. The site's server records every RSSI report its gateways deliver for those watches, together with the tester's labels.
The phone is the remote control and the labelling device. It does not scan Bluetooth and it does not store the recording.
A finished recording can be:
- replayed offline through any tracking algorithm (
tools/rtls-sim), and - installed as the site's fingerprint survey (used by
fingerprint_nb).
Where the recording lives
On the site's server, as one file per recording. Consequences:
- Recording continues when the phone sleeps, loses Wi-Fi, or Theseus is closed. Only one recording runs at a time per server.
- Any signed-in Theseus phone can join the running recording, label and stop it.
- The server stops a recording by itself after 2 hours, or when the file reaches its size cap.
- Nothing is written to the phone. Labels that could not be sent yet are held in memory by the live screen only.
- While the live screen is on screen the phone's screen stays on: a tester stands in a zone for a minute without touching the phone and must be able to tap Moving the moment they leave. Leaving the screen, or the app, gives the normal screen timeout back.
Before you start
- Sign in to the site: scan the site QR. The card is disabled until the session is ok.
- Wear each watch you record, or place it where the test happens. The server records what the gateways hear from the watch, not what the phone hears.
- Tracking does not have to run. When it is stopped, the recording still captures readings; the screen then has nothing to compare your labels with.
Walk-through
- On the start screen, tap RSSI recorder.
- If a recording is already running on the site, the screen says Recording in progress and names the watches it records (
Watch 1 (SN-W-0001)). Tap Join recording and continue at step 6. Otherwise tap New recording. - Pick watches. The list shows what the server can record, watches first: name, asset uid, type,
Heard 2 s agoorNot heard, and the zone tracking currently predicts. Search by name, asset uid or MAC. Select 1 to 10 (2 selected (max 10)). - Optionally tap Add a note in the bar at the bottom and type a note (at most 200 characters), for example the ward.
- Tap Start recording. If someone started a recording in the meantime, Theseus says so, names its watches and offers to join it.
- The live screen opens. The app bar and the heading above the zones say which watch is recorded (
Where is Watch 1 now? SN-W-0001). Walk into a zone and tap its button. The button is highlighted and counts how long the label has held:for 00:00. - When you leave the zone, tap Moving / between zones. Tap the next zone when you are inside it. A label holds until the next label for the same watch.
- Watch the top of the screen: Match or Mismatch says whether tracking agrees with your label. The list below shows which gateways hear the watch.
- When you are done, tap Stop and confirm. The summary opens.
- To use the recording as the fingerprint survey, tap Use as fingerprint survey and confirm. Then restart tracking on the site.
You can leave the live screen at any time (back). The recording continues on the server; join it again from the recorder's first screen.
The live screen
| Part | What it shows |
|---|---|
| App bar | Recording and, under it, the watch: Watch 1 · SN-W-0001, or 3 watches |
| Header | Time recorded so far, RSSI reports recorded, time until the server stops the recording by itself, and the connection: Live, Connecting, Offline, Site error |
| Watch bar (several watches only) | One chip per watch with its current label. The highlighted chip is the watch you carry. Tap another watch to give it a fixed zone, or to say you carry that one now |
| Verdict | Your label · for 00:42 and the zone on its own line (never cut to one line); what tracking predicts and with which algorithm; and the verdict block (below) |
| Gateways | The gateways that hear the watch right now, strongest first: signal bars, tag, the zone the gateway is in, RSSI, reports in the last 10 s (9 in 10 s), age of the latest report. Tap to show all (at most 12) |
| Zones | The heading with the watch, then (sites with several maps) the map selector, then one large button per zone of that map |
| Moving / between zones | Its own button, fixed at the bottom of the screen at every screen size and text size. On a short screen or with very large text the header and the verdict scroll with the zones instead |
| Stop | Top right. Asks first, and names the watch(es) |
Durations are written for 00:42. since is only used for times of day (since 14:15 on the start screen card).
Map selector
With several maps one full-width control says which map's zones are shown (Map 1 of 3 · Ward 4C). A tap opens the list of maps with the number of zones of each. The control is pinned above the zone buttons: it stays on screen however far the zones are scrolled.
Zone names
A zone name is never cut. A name gets up to three lines on its button; if a name of the map still does not fit, the grid uses fewer columns (three, two, one). Two zones that differ only at the end (… Bed 12, … Bed 13) can therefore always be told apart. A scrollbar shows when zones continue below Moving.
Verdict block
| Block | Meaning |
|---|---|
| Match | Tracking predicts the zone you labelled |
| Mismatch | Tracking predicts another zone |
| Unknown | You labelled a zone, tracking runs, but it does not place the watch in any zone |
| Moving | Your label is "moving": nothing is compared |
| No label | You have not labelled this watch yet. The panel asks for it (Tap the zone the watch is in), in the attention colour: a recording without labels has no ground truth |
| No tracking | Tracking is stopped on the site |
| Offline | The connection is down: the last prediction is too old to judge |
| Site error | The site answers, but with an error (HTTP 500, an unreadable answer). Not a Wi-Fi problem: the panel shows the site's message |
Match and mismatch differ by colour, icon and word.
Zone buttons
| Look | Meaning |
|---|---|
Highlighted, for mm:ss | The watch's current label and how long it has held |
| Small badge in the corner, tinted | No gateway inside this zone. These are the zones a survey is most needed for |
| Clock-and-arrow icon instead of the check mark | The label is not on the server yet |
Tapping a zone sends the label at once and the phone vibrates briefly. Tapping the zone that is already the label does nothing.
Gateways
No gateway hears this watchin red: check that the watch is switched on and that you picked the right one.- A gateway whose zone name is highlighted is inside the zone you labelled (screen readers hear "in your labelled zone").
- A zone name that does not fit beside the numbers moves to a second line instead of being cut.
- On a short screen the list is one line (
6 gateways · best GW-14 −58 dBm); tap it to open the list.
When the connection drops
The screen polls the server about every 2 seconds while you look at it. It stops polling when the app is in the background or another page covers the screen. None of this affects the recording.
- The verdict area says Connection lost — the recording continues on the server. The header says
Offline. The gateway list is dimmed and says how old it is. - Keep labelling. Every tap is highlighted at once, kept in order with the time of the tap, and sent when the connection returns. The server stamps each label with the moment you tapped, not the moment it arrived. The screen shows how many labels are waiting.
- A label is never given up on while the recording runs. The server accepts a label at most 10 minutes late (
age_msup to 600 s). A label that waited longer is still sent, as 10 minutes old: the server then holds the right zone from 10 minutes ago on, and the screen says that the time before that keeps the previous label. (Dropping it would leave the previous zone on the server for the rest of the recording.) - A label the server refuses (for example a zone that was deleted) is dropped and the screen says which one and why.
- If the recording stopped while labels were waiting, the screen says how many labels were not recorded.
- If the site reports another label for a watch than the one this phone last delivered, the screen says so (
The site now says Room 12 for Watch 1, not Room 13 as you last tapped…): another phone labelled the watch, or a delayed label arrived. Tap the right zone again if it is wrong. - Leaving the live screen with labels still waiting asks first: they are only in this screen's memory.
- Stop first gives labels that are still on their way up to 5 seconds to reach the server (
Sending…), then stops. What could not be sent is named on the summary. If the stop itself fails, the screen says so and the recording continues.
When the site answers with an error (HTTP 500, an answer Theseus cannot read), the screen says Site error and shows the site's message instead of "Connection lost": the tester should not go looking for Wi-Fi.
If the site answers 401, the field token was revoked: the screen says Session no longer valid and sends you back to the start screen, which then shows the session as revoked. Scan a new site QR (the old QR carries the revoked token).
Why a label cannot arrive late
A label means "this watch is in this zone from now on" and is stamped when it arrives (minus age_ms). A request Theseus gave up on must therefore never reach the server later, after newer labels. The recorder's HTTP transport (rtls_transport.dart) aborts a request when its time limit passes (10 s; 5 s for the status poll; 60 s for installing a survey): the connection is destroyed and nothing more is sent on it. The connection timeout (8 s) is shorter than the request limit. All recorder requests share one connection.
The contract has no sequence number on labels, so the server cannot reject a late or duplicate label itself; the notice above is the safety net.
The phone clock
age_ms is the difference of two readings of the phone clock. When the server's clock (from the status answers around the tap and at delivery) says something more than 10 s different, the phone clock was changed while the label waited (a time sync when the network returned), and the server's view is used.
Summary
For a stopped recording: start and stop time, duration, samples, labels, file size, why it stopped (Stopped by a tester, time limit, file size limit, server restart), the note, and the samples per watch (with a bar per watch when there are several). A watch with 0 samples was never heard. Labels the live screen could not deliver before the stop are named at the top until dismissed.
Use as fingerprint survey asks first. It replaces the site's fingerprint survey with the labelled data of this recording, and the new survey takes effect when tracking is restarted. The answer shows:
- how many zones the survey covers out of all zones,
- how many samples it holds and whether it replaced an earlier survey,
- whether tracking must be restarted,
- the server's warnings (for example a zone with very little data).
The server refuses a recording without any labelled zone time, and a recording that is still running.
Installing reads the whole recording on the server, so this request may take up to 60 seconds. When no answer arrives, Theseus does not say "not installed": the survey may have been installed, and the message says to check on the site before installing again.
Card on the start screen
| Card status | When |
|---|---|
Ready | Signed in, no recording known to run |
Recording…, badge REC, 2 watches · since 14:15 | The last thing the site said is that a recording is running, and its auto_stop_ms has not passed |
The start screen asks the site once each time the session becomes ok (one GET /theseus/rtls/recordings). It does not poll. Inside the tool every screen updates what the card shows. The card drops Recording… by itself at the time the server said it would stop the recording (auto_stop_ms). A recording that stopped earlier for another reason (size limit, another phone) shows as running until the tool is opened or the session is checked again. The recorder's first screen loads its list again when the app comes back from the background.
HTTP contract
The app follows Theseus RTLS recorder: HTTP contract (v1). The server side publishes it with the field APIs. In short:
| Method | Path under /api/v1 | Used for |
|---|---|---|
| GET | /theseus/rtls/layout | Maps, zones, gateways per zone |
| GET | /theseus/rtls/watches | Assets that can be recorded |
| POST | /theseus/rtls/recordings | Start (409 carries the running recording) |
| GET | /theseus/rtls/recordings | List, newest first |
| GET | /theseus/rtls/recordings/:id | Status: the live screen's poll |
| POST | /theseus/rtls/recordings/:id/labels | Label, with age_ms |
| POST | /theseus/rtls/recordings/:id/stop | Stop |
| POST | /theseus/rtls/recordings/:id/survey | Install as fingerprint survey |
All with Authorization: Bearer th_…, like the other field routes. The download route (…/download) is not used by the app.
Times on screen never use the phone clock as an absolute time: every answer carries server_time_ms, and the phone only measures how much time passed since the last answer or since a tap.
Where the contract does not say what "empty" looks like, the app accepts every form for values it only displays: a missing or null name, tag, note, mac, type or algorithm reads as empty (a gateway without a tag is shown by its MAC), a missing rssi_avg as rssi, missing bytes and zone geometry as 0, empty warnings are skipped. Ids, states, times, counts, zone ids and RSSI stay strict: a wrong one is an error that names the field.
A POST the server answers with HTTP 200 and type: success was done, even when the rest of the answer cannot be read: the label is not sent again, a started recording is joined, a stopped one is shown as stopped.
The contract's examples are in theseus/test/fixtures/rtls/*.json as complete HTTP bodies. The app's client tests read them; the server side can validate the same files.
Code
theseus/lib/tools/rssi_recorder/:
| File | Content |
|---|---|
rtls_models.dart | The contract's objects. What the app acts on is parsed strictly (a wrong field is an error state that names the field, never a crash); what it only shows may be empty |
rtls_client.dart | One method per route. Returns a value or a typed error (401, 400, 404, 409, server, network, timeout, malformed) |
rtls_transport.dart | Sends the requests on one shared connection and aborts a request at its time limit |
recorder_service.dart | App-level, the tool's own state (TheseusTool.createState): the shared client and the last known recording (for the card) |
live_controller.dart | The live screen's logic: status poll, label queue, verdict |
recorder_home_screen.dart | Join / new recording, previous recordings |
watch_picker_screen.dart | Pick watches, note, start |
live_screen.dart | The live screen: visibility, polling on and off, stop, leave |
live_widgets.dart | What the server says: header, verdict, gateways, notices |
live_zone_widgets.dart | What the tester taps: zone buttons, Moving, the sheet of another watch |
recorder_widgets.dart, recorder_format.dart | Shared pieces: error and 401 views, signal bars, clocks and counts as text |
summary_screen.dart | Summary and survey |
The registry entry is theseus/lib/tools/rssi_recorder_tool.dart. Keeping the screen on is theseus/lib/platform/keep_awake.dart with a method channel (theseus/keep_awake) in MainActivity.kt (FLAG_KEEP_SCREEN_ON) and AppDelegate.swift (isIdleTimerDisabled); no pub dependency.
Tests and screenshots
test/rtls_client_test.dart: every route against the fixtures, and every error path.test/rtls_transport_test.dart: a timed-out request is aborted (also on a real loopback socket) and requests share one connection.test/rssi_recorder_live_controller_test.dart: polling, the label queue (order,age_ms, labels older than 600 s, a label that arrives late, clock changes), verdicts, stop.test/rssi_recorder_live_test.dart: the live screen, including that Moving is on screen at every size and text scale, that no zone name is cut, and that the screen is kept on.test/rssi_recorder_test.dart: first screen, watch picker, summary, survey, card.test/support/fake_rtls_server.dart: the recorder routes in memory.
Screenshots (see start screen for the command): recorder_home_*, recorder_watch_picker_*, recorder_note_dialog_light, recorder_live_*, recorder_map_sheet_light, recorder_summary_*, recorder_survey_confirm_light, recorder_stop_confirm_light, and start_recording_running_light.
Non-goals
Scanning Bluetooth on the phone. Storing or sharing the recording file from the phone. Recording while signed out. Background polling or notifications from the recorder. Starting or stopping tracking from the phone. More than one recording per server at a time.