The robot's own access point, and the credentials for joining somebody else's.
A robot that has never been told about a network brings one up itself, and serves the whole web interface on it. So an unconfigured robot is a robot you can drive — join its network, open the address the panel is showing, and the drive page is there. Setting it up to join your own network is one page of that same interface rather than a separate mode it has to be talked into.
One radio, so one mode at a time
The RTL8723DU advertises both station and access point, but not at once, so
giving the robot a network of its own takes the access point away. That
makes a way back mandatory rather than a nicety, which is what
BB.NSK.Network.Monitor is for: a station configuration that never reaches
a connection falls back to the access point, so a mistyped passphrase costs a
minute rather than a reflash.
Nothing is stored
The SSID and the passphrase are derived from the OTP application and the board's serial number. They survive a reboot without being written anywhere, they differ between two robots on the same bench — which matters in a room full of them — and they can be recovered from a robot that has forgotten everything else.
The passphrase is a hash rather than the serial number's own digits, because
the serial is printed on things. It is drawn from an alphabet with no 0,
1, i, l or o in it, since somebody has to read it off a 400x300
e-paper panel and type it into a phone.
Where the credentials live
VintageNet persists what it is told to /root/vintage_net, and a persisted
configuration beats the one compiled into the firmware — so joining a network
outlives mix upload. It does not outlive a complete burn, which wipes the
application partition, so a freshly flashed robot is an access point again.
The access point itself is applied with persist: false. It is the state the
robot falls back to rather than a state it is ever put into, so persisting it
would let a bad afternoon become the permanent configuration.
Summary
Functions
The configuration for the robot's own access point.
The networks the last scan found, strongest first.
Where the robot answers on its own network.
Whether vintage_net is here to be driven.
The network the robot is set up to join, or nil when it isn't set up for one.
Whether the interface has reached a network, rather than merely being set up for one.
Forget the configured network and return to the access point.
The interface all of this configures.
Join a network, and remember it.
Which of the two the robot is currently set up for.
Which mode a particular vintage_net configuration describes.
The passphrase for the robot's own network.
Ask the radio to look for networks.
The name of the robot's own network.
Bring up the robot's own access point.
The configuration for joining somebody else's network.
The address to open once you have joined the robot's network.
Functions
@spec access_point() :: map()
The configuration for the robot's own access point.
@spec access_points() :: [map()]
The networks the last scan found, strongest first.
@spec address() :: :inet.ip4_address()
Where the robot answers on its own network.
@spec available?() :: boolean()
Whether vintage_net is here to be driven.
False on the host, where the setup page is built and looked at but no radio
exists, so everything that reaches for it says so rather than raising
UndefinedFunctionError into a LiveView.
@spec configured_ssid() :: String.t() | nil
The network the robot is set up to join, or nil when it isn't set up for one.
@spec connected?() :: boolean()
Whether the interface has reached a network, rather than merely being set up for one.
@spec forget() :: :ok | {:error, term()}
Forget the configured network and return to the access point.
@spec interface() :: String.t()
The interface all of this configures.
Join a network, and remember it.
The access point goes away as this takes effect, so whoever called this over it will lose their connection whether or not the new credentials are any good.
@spec mode() :: :access_point | :station | :unconfigured
Which of the two the robot is currently set up for.
:unconfigured is a robot that has never been told anything, which is the
state a freshly burned one is in until BB.NSK.Network.Monitor brings the
access point up.
A VintageNetWiFi interface with no networks counts as unconfigured, which
is what it is: an interface that has been declared and never told what to join.
mix nerves.new writes exactly that — {"wlan0", %{type: VintageNetWiFi}} —
into config/target.exs, and reading it as :station costs a freshly burned
robot a full connection timeout of doing nothing before it gives up on a
network it was never told about and brings up its own.
@spec mode(map() | nil) :: :access_point | :station | :unconfigured
Which mode a particular vintage_net configuration describes.
Split out from mode/0 so it can be reasoned about without a radio.
@spec passphrase() :: String.t()
The passphrase for the robot's own network.
Derived from the serial number, so it is stable across reboots and different on every robot, and shown on the panel because that is the only place anybody can read it from.
@spec scan() :: :ok | {:error, term()}
Ask the radio to look for networks.
Results don't come back from this — they arrive asynchronously and are read
with access_points/0.
Scanning works while the robot is its own access point, confirmed on hardware 2026-09-17. That is not a given: one radio is doing both jobs, and a chip that refused would have been within its rights. Nothing above this depends on it anyway — the setup page takes a typed SSID, because a hidden network never appears in a scan whatever the radio can do.
@spec ssid() :: String.t()
The name of the robot's own network.
The robot's name, then enough of the serial number to tell two robots apart — which a workshop full of them very much needs.
The name comes from BB.NSK.name/0, which bb_nsk.install sets to the host
application. It cannot be derived here: this module's own OTP application is
:bb_nsk, so every robot on the bench would advertise the same one.
@spec start_access_point() :: :ok | {:error, term()}
Bring up the robot's own access point.
Deliberately not persisted — see the moduledoc.
The configuration for joining somebody else's network.
An empty passphrase means an open network, which plenty of cafes and a few workshops still are.
@spec url() :: String.t()
The address to open once you have joined the robot's network.