add: Cellular Network support (rebase of #2765) - #3096
Conversation
Bump the network API version, drop the deleted term freeze file, regenerate the settings search index, and reuse existing translation terms where they already cover the new cellular UI.
Assigning SettingsData.networkPreference through the readonly alias threw on click, so the Cellular segment in Control Center did nothing.
The Control Center type switcher was bound only to preference, so Cellular snapped back to Ethernet. The settings modem row highlighted on hover but had no click handler. Show the modem as a connect target when no APN profiles exist.
Clicking Mobile broadband only activated an existing profile, so a modem with no APN did nothing useful. Fall back to AddAndActivate like ethernet connect.
| } | ||
| } | ||
|
|
||
| b.cellularDevices[iface] = &cellularDeviceInfo{ |
There was a problem hiding this comment.
This map access needs to be protected. Either uses the syncmap from dankgo or protect with RW mutex
There was a problem hiding this comment.
Done in e922cc9. Added cellularDevicesSnapshot() / cellularDeviceByIface() / setCellularDeviceInfo() / removeCellularDeviceByPath() guarded by the existing devMutex, mirroring the ethernet and wifi accessors already in this file — cellularDevices is now covered by the same pattern rather than a second mechanism. Every raw access is converted: Initialize, both signal-pump loops, handleDeviceAdded, handleDeviceRemoved, updateCellularState, updateAllCellularDevices and DisconnectCellularDevice. Also updated the devMutex doc comment to mention the map.
Happy to switch to syncmap instead if you would rather the new map not extend devMutex.
| // CancelCredentials provides a mock function for the type MockBackend | ||
| func (_mock *MockBackend) CancelCredentials(token string) error { | ||
| ret := _mock.Called(token) | ||
| // ActivateCellularConnection provides a mock function with given fields: uuid |
There was a problem hiding this comment.
This file shouldnt be hand-edited its generated. Re-generate mocks with mockery v3 just by running mockery e.g. go run github.com/vektra/mockery/v3@latest
This info about mockery was missing from CONTRIBUTING.md but I just added it on master
There was a problem hiding this comment.
Done in e922cc9 — regenerated with go run github.com/vektra/mockery/v3@latest (v3.7.3) instead of hand-editing. The file is now purely additive against master (395 insertions, 0 deletions); the hand-edited version had drifted from the generator output.
One note: mockery also regenerates internal/mocks/internal/plugins/mock_GitClient.go, which is stale on master and unrelated to cellular. I reverted that one to keep this PR focused — worth a separate regen commit on master.
| StyledText { | ||
| width: parent.width | ||
| visible: (NetworkService.cellularDevices?.length ?? 0) === 0 && cellularConnectionsModel.values.length === 0 | ||
| text: I18n.tr("No devices found") |
There was a problem hiding this comment.
I feel like this text could render twice when theres no modem and no saved config (line 632 also)
There was a problem hiding this comment.
Good catch, confirmed. The label at the top of cellularColumn was gated only on cellularDevices.length === 0, while this one also checks the connections model — so with no modem and no saved config both were visible. Dropped the top one in e922cc9; this one's condition was already the correct combined check.
| } | ||
|
|
||
| func (b *NetworkManagerBackend) refreshCellularState() { | ||
| b.updateAllCellularDevices() |
There was a problem hiding this comment.
Re-use this method in more places, some places are repeating this logic
There was a problem hiding this comment.
Done in e922cc9. refreshCellularState() is now used in SetCellularEnabled, DisconnectCellular, DisconnectCellularDevice, ActivateCellularConnection and the device-removal path in backend_networkmanager_signals.go.
I left the remaining sites in signals.go alone on purpose — those interleave ethernet/wifi/hotspot updates and fire a single onStateChange at the end, so folding the helper in would emit an extra mid-sequence broadcast.
| if _, err := nm.AddAndActivateConnection(settings, dev); err != nil { | ||
| delete(settings["gsm"], "auto-config") | ||
| if _, retryErr := nm.AddAndActivateConnection(settings, dev); retryErr != nil { | ||
| return fmt.Errorf("failed to create and activate cellular connection: %w", err) |
There was a problem hiding this comment.
This should wrap retryErr, seems like it'd be more indicative of the real error
There was a problem hiding this comment.
Agreed — fixed in e922cc9, it now wraps retryErr. Also added a short comment noting the retry exists because older NetworkManager/ModemManager stacks reject gsm.auto-config.
| width: enableCellularLabel.implicitWidth + Theme.spacingL * 2 | ||
| height: enableCellularLabel.implicitHeight + Theme.spacingM * 2 | ||
| radius: height / 2 | ||
| color: enableCellularButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) |
There was a problem hiding this comment.
Instead of hand rolling Qt.rgba re-use the appropriate tokens like wifiOffContent does Theme.surfaceTextSecondary/primaryHover/primaryHoverLight
There was a problem hiding this comment.
Done in e922cc9 — the icon now uses Theme.surfaceTextSecondary and the button uses Theme.primaryHover / Theme.primaryHoverLight, matching wifiOffContent.
- Guard cellularDevices map with devMutex via snapshot/by-iface/set/remove helpers, mirroring the existing ethernet and wifi accessors - Regenerate mocks with mockery v3 instead of hand-editing mock_Backend.go - Reuse refreshCellularState() in SetCellularEnabled, DisconnectCellular, DisconnectCellularDevice, ActivateCellularConnection and device removal - Wrap retryErr rather than the first error when the auto-config retry fails - Use Theme.surfaceTextSecondary/primaryHover/primaryHoverLight in the cellular-off state instead of hand-rolled Qt.rgba values - Drop the duplicate "No devices found" label so it can no longer render twice with no modem and no saved config Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Still testing |
Hardware testing doneRe-tested on the Fibocom L831-EAU-00 (Vodacom SA, Build / unit
Cellular against the real modemDriven over the backend socket (API v31, NetworkManager backend), starting from a deleted GSM profile so the create-from-scratch path was exercised:
All four UIControl Center cellular panel renders correctly in both states:
Two things I could not prove on this hardware
|
| cellularToggling = false; | ||
|
|
||
| if (response.error) { | ||
| ToastService.showError(I18n.tr("Failed to enable cellular"), response.error); |
There was a problem hiding this comment.
The error is always "failed to enable cellular" even if its a failed to disable. I would just replace the string with "Failed to toggle cellular"
There was a problem hiding this comment.
Done in 29bc88d — the error toast is now Failed to toggle cellular.
| } | ||
| } | ||
|
|
||
| function enableCellularRadio() { |
There was a problem hiding this comment.
Not used so I'd just delete it, in addition to the function in DMSNetworkService
There was a problem hiding this comment.
Done in 29bc88d — removed enableCellularRadio() from both NetworkService.qml and DMSNetworkService.qml. The backend network.cellular.enable IPC handler is still there.
| types.push("cellular"); | ||
| return types.length > 0 ? types : ["wifi"]; | ||
| } | ||
| property int selectedTypeIndex: -1 |
There was a problem hiding this comment.
Think it would be more robust to store the selected value instead of index
property string selectedType: ""
readonly property string currentConnectionType: {
if (selectedType && connectionTypes.includes(selectedType))
return selectedType;
return connectionTypes[Math.max(0, currentPreferenceIndex)] || "wifi";
}There was a problem hiding this comment.
Done in 29bc88d — selectedType is now a string, and currentConnectionType uses it when it is still in connectionTypes, otherwise it falls back to preference. If a NIC disappears while the panel is open the pane no longer remaps to a different medium via a stale index.
| if (!selected) | ||
| return; | ||
| NetworkService.setNetworkPreference(index === 0 ? "ethernet" : "wifi"); | ||
| selectedTypeIndex = index; |
There was a problem hiding this comment.
And here instead of index:
currentIndex: Math.max(0, connectionTypes.indexOf(currentConnectionType))
onSelectionChanged: (index, selected) => {
if (!selected)
return;
selectedType = connectionTypes[index] || "wifi";
NetworkService.setNetworkPreference(selectedType);
}There was a problem hiding this comment.
Done in 29bc88d — currentIndex is connectionTypes.indexOf(currentConnectionType), and onSelectionChanged writes selectedType from the chosen value.
- Toast "Failed to toggle cellular" instead of always "enable" - Remove unused enableCellularRadio() wrappers - Store Control Center network type as a value, not a list index
Description
Rebased and review-fixed version of #2765 (cellular network support).
Original work by @yechielw. Rebased onto current
masterand tested on a machine with a Fibocom L831-EAU-00 (Vodacom LTE).Review from @bbedward on #2765:
master(term_freeze.jsonis gone)APIVersion30 → 31settings_search_index.jsonviaextract_settings_index.pywpa_supplicantcellular stubs so theBackendinterface still compilesFixes found while testing:
userPreferencealias (Cellular chip was a no-op)AddAndActivateConnectionlike Ethernet connectType of change
Related issues
Continues / supersedes #2765
Checklist
I18n.tr(), reusing existing terms where possiblego test ./internal/server/...passed