Skip to content
This repository has been archived by the owner on Oct 24, 2024. It is now read-only.

Commit

Permalink
[RSDK-7680] [APP-4933] Allow non-internet wifi to count as "connected…
Browse files Browse the repository at this point in the history
…", add roaming mode (#18)
  • Loading branch information
Otterverse authored Jun 25, 2024
1 parent d08eb8f commit aceac0b
Show file tree
Hide file tree
Showing 17 changed files with 1,493 additions and 1,094 deletions.
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ else ifeq ($(GOARCH),arm64)
LINUX_ARCH = aarch64
endif

GIT_REVISION = $(shell git rev-parse HEAD | tr -d '\n')
TAG_VERSION ?= $(shell git tag --points-at | sort -Vr | head -n1 | cut -c2-)
ifeq ($(shell git status -s),)
TAG_VERSION ?= $(shell git tag --points-at | sort -Vr | head -n1 | cut -c2-)
GIT_REVISION = $(shell git rev-parse HEAD | tr -d '\n')
endif
ifeq ($(TAG_VERSION),)
PATH_VERSION = custom
else
Expand All @@ -30,7 +32,7 @@ arm64:
amd64:
make GOARCH=amd64

bin/viam-agent-provisioning-$(PATH_VERSION)-$(LINUX_ARCH): go.* *.go */*.go */*/*.go portal/templates/*
bin/viam-agent-provisioning-$(PATH_VERSION)-$(LINUX_ARCH): go.* *.go */*.go */*/*.go networkmanager/templates/*
go build -o $@ -tags $(TAGS) -ldflags $(LDFLAGS) ./cmd/viam-agent-provisioning/main.go
test "$(PATH_VERSION)" != "custom" && cp $@ bin/viam-agent-provisioning-stable-$(LINUX_ARCH) || true

Expand Down
106 changes: 61 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,24 @@ Lastly, if the script cannot detect your mountpoint, you can specify it directly
Note: On all symlink operations, be sure to use relative symlinks, especially if working on a mounted image (e.g. not a live/booted system.)


#### Factory/Manufacturer Configuration
`/etc/viam-provisioning.json` can be placed on a device image to customize the provisioning experience.
## Configuration
No configuration is typically neccessary for normal use. Provisioning mode will start a hotspot when either not configured (no /etc/viam.json) or not online.

`/etc/viam-provisioning.json` can be placed on a device/image to customize the provisioning (default) experience before a device is first connected. These values can be overridden later via "attributes" in the agent-provisioning subsystem in the device's config. See the Additional Networks section below for an example using config via cloud "attributes."

Example `/etc/viam-provisioning.json`
```json
{
"manufacturer": "Skywalker",
"model": "C-3PO",
"fragment_id": "2567c87d-7aef-41bc-b82c-d363f9874663",
"disable_dns_redirect": true,
"hotspot_prefix": "skywalker-setup",
"hotspot_password": "skywalker123"
"disable_dns_redirect": true,
"hotspot_password": "skywalker123",
"roaming_mode": false,
"offline_timeout": "3m30s",
"user_timeout": "2m30s",
"fallback_timeout": "15m"
}
```
* All fields are optional (as is the entire file.) Values not set will use defaults.
Expand All @@ -68,63 +74,73 @@ Example `/etc/viam-provisioning.json`
* Purely informative. May be displayed on captive portal and/or mobile app.
* fragment_id: No default.
* Corresponds to a fragment_id in the Viam cloud. If present, mobile app can pre-configure a robot for a user by using this.
* hotspot_prefix: Defaults to `viam-setup`
* Will have the hostname of the device append and be used for the provisioning hotspot SSID.
* disable_dns_redirect: Defaults to false.
* By default, ALL DNS lookups via the provisioning hotspot will redirect to the device. This causes most phones/mobile devices to automatically redirect the user to the captive portal as a "sign in" screen.
* When disabled, only domains ending in `.setup` (ex: `viam.setup`) will be redirected. This generally avoids displaying the portal to users and is mainly used in conjunction with a mobile provisioning application workflow.
* hotspot_prefix: Defaults to `viam-setup`
* Will have the hostname of the device append and be used for the provisioning hotspot SSID.
* hotspot_password: Defaults to `viamsetup`
* Wifi password for provisioning hotspot.

## Mobile App Provisioning
If you are using the Viam mobile app, and your device has been pre-installed with the agent (per above steps) you can use the mobile app to configure your robot, instead of the captive web portal.

### Steps in Viam mobile app
Download the Viam mobile app from [Apple App Store](https://apps.apple.com/vn/app/viam-robotics/id6451424162) or [Google Play](https://play.google.com/store/apps/details?id=com.viam.viammobile&hl=en&gl=US).

Within the [Viam mobile app](https://docs.viam.com/fleet/#the-viam-mobile-app) once logged in, navigate to an organization, then to a location, then inside any location tap "Add new smart machine", and follow the instructions in app. See the mobile app documentation for further details.

## User Configuration
No configuration is neccessary for normal end-user use. Provisioning mode will start a hotspot when either not configured (no /etc/viam.json) or not online.
* roaming_mode: Defaults to false.
* By default, the device will only attempt to connect to a single wifi network (the one with the highest priority), usually provided during initial provisioning/setup. Wifi connection alone is enough to consider the device as "online" even if the global internet is not reachable.
* When enabled, the device will attempt connections to all configured networks, and only considers the device online if the internet is reachable.
* offline_timeout: Defaults to "2m" (2 minutes)
* Will only enter provisioning mode (hotspot) after being disconnected longer than this time.
* Useful on flaky connections, or when part of a system where the device may start quickly, but the wifi/router may take longer to be available.
* user_timeout: Defaults to "5m" (5 minutes)
* Amount of time before considering a user (using the provisioning portal via web or mobile app) idle, and resuming normal behavior.
* Used to avoid interrupting provisioning mode (e.g. for network tests/retries) when a user might be busy entering details.
* fallback_timeout: Defaults to "10m" (10 minutes)
* Provisioning mode will exit after this time, to allow other unmanaged (e.g. wired) or manually configured connections to be tried.
* Provisioning mode will restart if the connection/online status doesn't change.
* networks: Defaults to none.
* See "Additional Networks" below.

### Additional Networks (Optional)
To add additional networks to an already-online device, go to the "Raw JSON" button on the Config tab for your robot/device in https://app.viam.com
To add additional networks to an already-online device, go to the JSON editor for your device's config in https://app.viam.com

From there, add an `attributes` field to the agent-provisioning subsystem, using the example below.

```json
"agent_config": {
"subsystems": {
"agent-provisioning": {
"release_channel": "stable",
"pin_version": "",
"pin_url": "",
"disable_subsystem": false,
"attributes": {
"hotspot_password": "testpass",
"networks": [
{
"type": "wifi",
"ssid": "primaryNet",
"psk": "myFirstPassword",
"priority": 30
},
{
"type": "wifi",
"ssid": "fallbackNet",
"psk": "mySecondPassword",
"priority": 10
}
]
"agent": {
"agent-provisioning": {
"release_channel": "stable",
"attributes": {
"hotspot_password": "testpass",
"roaming_mode": true,
"networks": [
{
"type": "wifi",
"ssid": "primaryNet",
"psk": "myFirstPassword",
"priority": 30
},
{
"type": "wifi",
"ssid": "fallbackNet",
"psk": "mySecondPassword",
"priority": 10
}
},
]
}
}
}
```
Note: the `hotspot_password` overrides the default password (either the default `viamsetup` or any factory-customized password) used to connect to the hotspot if you wish to further secure things. It is optional and can be omitted entirely.

Note that adding additional networks is mostly useless unless you've also enabled roaming_mode as shown. Otherwise, the default behavior will only attempt to connect to the highest priority network. In non-roaming mode, a network added directly on the device during provisioning is set to 999 priority.

## Use
Provisioning mode will start a hotspot when either not configured (no /etc/viam.json) or not online. By default, the wifi SSID will be `viam-setup-$HOSTNAME`. The default password is `viamsetup`. After connecting with a mobile device, you should be redirected to a sign-in page. If you are using a laptop or are not redirected, try opening http://viam.setup/ in a browswer. From the portal page, you can select an SSID and provide a password to allow your device to connect. The setup hotspot will disappear (and disconnect your mobile device) while the device attempts connection. If the hotspot reappears, there may be have been an issue or invalid password. Please try again.
Provisioning mode will start a hotspot when either not configured (no /etc/viam.json) or not online. By default, the wifi SSID will be `viam-setup-$HOSTNAME`. The default password is `viamsetup`. After connecting to the hotspot with a mobile device, you should be redirected to a sign-in page. If you are using a laptop or are not redirected, try opening http://viam.setup/ in a browswer. From the portal page, you can select an SSID and provide a password to allow your device to connect. The setup hotspot will disappear (and disconnect your mobile device) while the device attempts connection. If the hotspot reappears, there may be have been an issue or invalid password. Please try again.

### Mobile App Provisioning
If you are using the Viam mobile app, and your device has been pre-installed with the agent (per above steps) you can use the mobile app to configure your robot, instead of the captive web portal.

#### Steps in Viam mobile app
Download the Viam mobile app from [Apple App Store](https://apps.apple.com/vn/app/viam-robotics/id6451424162) or [Google Play](https://play.google.com/store/apps/details?id=com.viam.viammobile&hl=en&gl=US).

Within the [Viam mobile app](https://docs.viam.com/fleet/#the-viam-mobile-app) once logged in, navigate to an organization, then to a location, then inside any location tap "Add new smart machine", and follow the instructions in app. See the mobile app documentation for further details.

### Pre-installed provisioning
### Pre-installed provisioning (Via web portal, not mobile app)
If there is no `/etc/viam.json` present, the captive portal will also require you to paste the content of the viam-server config to use in `/etc/viam.json` This can be copied from the "Setup" tab of your machine in https://app.viam.com by clicking the "Copy viam-server configuration" button near the top right.

### Test CLI Utility
Expand Down
4 changes: 3 additions & 1 deletion cmd/client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
pb "go.viam.com/api/provisioning/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

"github.com/viamrobotics/agent-provisioning/networkmanager"
)

func main() {
Expand Down Expand Up @@ -133,7 +135,7 @@ func SetDeviceCreds(ctx context.Context, client pb.ProvisioningServiceClient, id

func SetWifiCreds(ctx context.Context, client pb.ProvisioningServiceClient, ssid, psk string) {
req := &pb.SetNetworkCredentialsRequest{
Type: "wifi",
Type: networkmanager.NetworkTypeWifi,
Ssid: ssid,
Psk: psk,
}
Expand Down
22 changes: 11 additions & 11 deletions cmd/viam-agent-provisioning/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,31 +66,31 @@ func main() {
ctx := setupExitSignalHandling()
defer activeBackgroundWorkers.Wait()

pCfg, err := provisioning.LoadProvisioningConfig(opts.ProvisioningConfig)
// Manufacturer settings from agent-provisioning.json
pCfg, err := provisioning.LoadConfig(provisioning.DefaultConf, opts.ProvisioningConfig)
if err != nil {
log.Error(errw.Wrapf(err, "error loading %s, using defaults", opts.ProvisioningConfig))
}

cfg, err := provisioning.LoadConfig(opts.Config)
// User settings from the "attributes" section of the cloud config (passed from parent agent via json file)
cfg, err := provisioning.LoadConfig(*pCfg, opts.Config)
if err != nil {
log.Warn(err)
log.Error(errw.Wrapf(err, "error loading %s, using defaults", opts.Config))
}

// If user settings override the hotspot password, use that instead
if cfg.HotspotPassword != "" {
pCfg.HotspotPassword = cfg.HotspotPassword
}

nm, err := netman.NewNMWrapper(ctx, log, pCfg, opts.AppConfig)
nm, err := netman.NewNMWrapper(ctx, log, cfg, opts.AppConfig)
if err != nil {
log.Error(err)
return
}
defer nm.Close()

if !cfg.RoamingMode && len(cfg.Networks) > 0 {
log.Warn("Additional networks configured, but Roaming Mode is not enabled. Additional wifi networks will likely be unused.")
}

for _, network := range cfg.Networks {
log.Debugf("adding/updating NetworkManager configuration for %s", network.SSID)
if err := nm.AddOrUpdateConnection(network); err != nil {
if _, err := nm.AddOrUpdateConnection(network); err != nil {
log.Error(errw.Wrapf(err, "error adding network %s", network.SSID))
}
}
Expand Down
118 changes: 118 additions & 0 deletions networkmanager/definitions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package networkmanager

import (
"encoding/binary"
"errors"
"net/http"
"sync"
"sync/atomic"
"time"

gnm "github.com/Otterverse/gonetworkmanager/v2"
"go.uber.org/zap"
pb "go.viam.com/api/provisioning/v1"
"google.golang.org/grpc"

provisioning "github.com/viamrobotics/agent-provisioning"
)

// This file contains type, const, and var definitions.

const (
DNSMasqFilepath = "/etc/NetworkManager/dnsmasq-shared.d/80-viam.conf"
DNSMasqContentsRedirect = "address=/#/10.42.0.1\n"
DNSMasqContentsSetupOnly = "address=/.setup/10.42.0.1\n"

ConnCheckFilepath = "/etc/NetworkManager/conf.d/80-viam.conf"
ConnCheckContents = "[connectivity]\nuri=http://packages.viam.com/check_network_status.txt\ninterval=300\n"

NetworkTypeWifi = "wifi"
NetworkTypeHotspot = "hotspot"
)

var (
BindAddr = "10.42.0.1"
// older networkmanager requires unit32 arrays for IP addresses.
IPAsUint32 = binary.LittleEndian.Uint32([]byte{10, 42, 0, 1})
ErrBadPassword = errors.New("bad or missing password")
ErrConnCheckDisabled = errors.New("NetworkManager connectivity checking disabled by user, network management will be unavailable")
ErrNoActiveConnectionFound = errors.New("no active connection found")
mainLoopDelay = time.Second * 1
scanLoopDelay = time.Second * 15
connectTimeout = time.Second * 50 // longer than the 45 second timeout in NetworkManager
)

type NMWrapper struct {
monitorWorkers sync.WaitGroup
provisioningWorkers sync.WaitGroup

// blocks start/stop/etc operations
// holders of this lock must use HealthySleep to respond to HealthChecks from the parent agent during long operations
opMu sync.Mutex

// only set during NewNMWrapper, no lock
nm gnm.NetworkManager
dev gnm.DeviceWireless
settings gnm.Settings
hostname string
logger *zap.SugaredLogger
cfg provisioning.Config
viamCfgPath string

// internal locking
state *connectionState

// locking for data updates
dataMu sync.Mutex
networks map[string]*network
hotspotSSID string
activeSSID string
lastSSID string
primarySSID string
errors []error

// portal
webServer *http.Server
grpcServer *grpc.Server

input *provisioning.UserInput
inputReceived atomic.Bool
banner string
pb.UnimplementedProvisioningServiceServer
}

type network struct {
netType string
ssid string
security string
signal uint8
priority int32
isHotspot bool

firstSeen time.Time
lastSeen time.Time

lastTried time.Time
connected bool
lastConnected time.Time
lastError error

conn gnm.Connection
activeConn gnm.ActiveConnection
}

func (n *network) getInfo() provisioning.NetworkInfo {
var errStr string
if n.lastError != nil {
errStr = n.lastError.Error()
}

return provisioning.NetworkInfo{
Type: n.netType,
SSID: n.ssid,
Security: n.security,
Signal: int32(n.signal),
Connected: n.connected,
LastError: errStr,
}
}
Loading

0 comments on commit aceac0b

Please sign in to comment.