Author SHA1 Message Date
ztimson 14b8eefb77 Update fallback-ap.sh
Code review / review (pull_request) Successful in 20s
2026-09-12 19:58:03 -04:00
ztimson d1e1842052 Merge pull request 'Update install.sh' (#14) from ztimson-patch-13 into master
Reviewed-on: #14
2026-09-12 19:57:49 -04:00
ztimson ca3f2d15a8 Update install.sh
Code review / review (pull_request) Successful in 20s
2026-09-12 19:56:47 -04:00
ztimson 1107712e75 Merge pull request 'Update install.sh' (#12) from ztimson-patch-12 into master
Reviewed-on: #12
2026-09-12 19:55:16 -04:00
ztimson bd19cc87e3 Update install.sh
Code review / review (pull_request) Successful in 33s
2026-09-12 19:55:10 -04:00
ztimson 1e4b9394db Merge pull request 'Update install.sh' (#11) from ztimson-patch-11 into master
Reviewed-on: #11
2026-09-12 19:54:06 -04:00
ztimson b83210d0eb Update install.sh
Code review / review (pull_request) Successful in 46s
2026-09-12 19:53:58 -04:00
ztimson 9d2987323e Merge pull request 'Update install.sh' (#10) from ztimson-patch-10 into master
Reviewed-on: #10
2026-09-12 19:51:05 -04:00
ztimson b319455ef3 Update install.sh
Code review / review (pull_request) Successful in 32s
2026-09-12 19:51:00 -04:00
ztimson 825fe96dee Merge pull request 'ztimson-patch-9' (#9) from ztimson-patch-9 into master
Reviewed-on: #9
2026-09-12 19:47:51 -04:00
ztimson 8859375919 Update fallback-ap.sh
Code review / review (pull_request) Successful in 30s
2026-09-12 19:47:42 -04:00
ztimson 66b4cedd39 Update install.sh 2026-09-12 19:47:26 -04:00
ztimson 3243309bbb Merge pull request 'Update fallback-ap.sh' (#8) from ztimson-patch-8 into master
Reviewed-on: #8
2026-09-12 19:46:03 -04:00
ztimson 8b7702e90c Update fallback-ap.sh
Code review / review (pull_request) Successful in 35s
2026-09-12 19:45:56 -04:00
ztimson ed20d616f5 Merge pull request 'Update install.sh' (#7) from ztimson-patch-7 into master
Reviewed-on: #7
2026-09-12 19:40:29 -04:00
ztimson 0180f81420 Update install.sh
Code review / review (pull_request) Successful in 32s
2026-09-12 19:40:23 -04:00
ztimson a47fea04fb Merge pull request 'ztimson-patch-6' (#6) from ztimson-patch-6 into master
Reviewed-on: #6
2026-09-12 19:31:08 -04:00
ztimson 985c6b4ca7 Update fallback-ap.sh
Code review / review (pull_request) Successful in 32s
2026-09-12 19:30:45 -04:00
ztimson 6fca943d2c Update install.sh 2026-09-12 19:30:06 -04:00
ztimson 5c6862306d Merge pull request 'Update install.sh' (#5) from ztimson-patch-5 into master
Reviewed-on: #5
2026-09-12 19:22:30 -04:00
ztimson 96c6b86d40 Update install.sh
Code review / review (pull_request) Successful in 21s
2026-09-12 19:22:25 -04:00
ztimson 255385bf82 Merge pull request 'Update README.md' (#4) from ztimson-patch-4 into master
Reviewed-on: #4
2026-09-12 19:20:50 -04:00
ztimson d0e2752cd8 Update README.md
Code review / review (pull_request) Successful in 27s
2026-09-12 19:20:45 -04:00
ztimson 8758a43088 Merge pull request 'Add install.sh' (#3) from ztimson-patch-3 into master
Reviewed-on: #3
2026-09-12 19:18:55 -04:00
ztimson 4f79a652e5 Add install.sh
Code review / review (pull_request) Successful in 23s
2026-09-12 19:18:48 -04:00
ztimson 6e201723d9 Merge pull request 'Update README.md' (#2) from ztimson-patch-2 into master
Reviewed-on: #2
2026-09-12 19:16:11 -04:00
ztimson 69372f0b94 Update README.md
Code review / review (pull_request) Successful in 28s
2026-09-12 19:16:05 -04:00
ztimson 63a3dec564 Merge pull request 'Add fallback-ap.sh' (#1) from ztimson-patch-1 into master
Reviewed-on: #1
2026-09-12 19:12:27 -04:00
ztimson 37f1a7e646 Add fallback-ap.sh
Code review / review (pull_request) Successful in 1m2s
2026-09-12 19:12:08 -04:00
3 changed files with 659 additions and 79 deletions
+31 -77
View File
@@ -3,130 +3,84 @@
<br />
<!-- Logo -->
<img src="https://git.zakscode.com/repo-avatars/2b4ee6ba1f2e2618bf7694e4a52fb56d1d0ea6abafa2dcbe496ab786b86d5a76" alt="Logo" width="200" height="200">
<img src="https://git.zakscode.com/repo-avatars/80775ae9cf0d2c4fb4aec035efb650e631d4d0046ab6fc04c6c041e91ba11d44" alt="Logo" width="200" height="200">
<!-- Title -->
### Template
### Fallback AP
<!-- Description -->
Simple repository template
Automatic fallback Wi-Fi access point for headless Linux devices
<!-- Repo badges -->
[![Version](https://img.shields.io/badge/dynamic/json.svg?label=Version&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/template/tags&query=$[0].name)](https://git.zakscode.com/ztimson/template/tags)
[![Pull Requests](https://img.shields.io/badge/dynamic/json.svg?label=Pull%20Requests&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/template&query=open_pr_counter)](https://git.zakscode.com/ztimson/template/pulls)
[![Issues](https://img.shields.io/badge/dynamic/json.svg?label=Issues&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/template&query=open_issues_count)](https://git.zakscode.com/ztimson/template/issues)
[![Version](https://img.shields.io/badge/dynamic/json.svg?label=Version&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/fallback-ap/tags&query=$[0].name)](https://git.zakscode.com/ztimson/fallback-ap/tags)
[![Pull Requests](https://img.shields.io/badge/dynamic/json.svg?label=Pull%20Requests&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/fallback-ap&query=open_pr_counter)](https://git.zakscode.com/ztimson/fallback-ap/pulls)
[![Issues](https://img.shields.io/badge/dynamic/json.svg?label=Issues&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/fallback-ap&query=open_issues_count)](https://git.zakscode.com/ztimson/fallback-ap/issues)
<!-- Links -->
---
<div>
<a href="https://git.zakscode.com/ztimson/template/wiki" target="_blank">Documentation</a>
• <a href="https://git.zakscode.com/ztimson/template/releases" target="_blank">Release Notes</a>
• <a href="https://git.zakscode.com/ztimson/template/issues/new?template=.github%2fissue_template%2fbug.md" target="_blank">Report a Bug</a>
• <a href="https://git.zakscode.com/ztimson/template/issues/new?template=.github%2fissue_template%2fenhancement.md" target="_blank">Request a Feature</a>
<a href="https://git.zakscode.com/ztimson/fallback-ap/wiki" target="_blank">Documentation</a>
• <a href="https://git.zakscode.com/ztimson/fallback-ap/releases" target="_blank">Release Notes</a>
• <a href="https://git.zakscode.com/ztimson/fallback-ap/issues/new?template=.github%2fissue_template%2fbug.md" target="_blank">Report a Bug</a>
• <a href="https://git.zakscode.com/ztimson/fallback-ap/issues/new?template=.github%2fissue_template%2fenhancement.md" target="_blank">Request a Feature</a>
</div>
---
</div>
## Table of Contents
- [Template](#top)
- [Fallback AP](#top)
- [About](#about)
- [Demo](#demo)
- [Built With](#built-with)
- [Setup](#setup)
- [Production](#production)
- [Development](#development)
- [Installation](#installation)
- [Usage](#usage)
- [License](#license)
## About
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
A NetworkManager-based script for headless Linux devices (e.g. a Raspberry Pi) that keeps them reachable even when their configured Wi-Fi network isn't available. When the device loses its Wi-Fi connection, it automatically stands up a local access point so you can connect directly and reconfigure it. While that access point has no clients connected, it periodically steps aside for a short window to let NetworkManager try reconnecting to a known network in the background, then brings the access point back up if nothing was found.
### Demo
Website: https://git.zakscode.com
Runs as a `systemd` service and is managed entirely through `nmcli`/`iw` — no extra daemons (`hostapd`, `dnsmasq`, etc.) required.
### Built With
[![Angular](https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular)](https://angular.io/)
[![Android](https://img.shields.io/badge/android-34A853?style=for-the-badge&logo=android&logoColor=ffffff)](https://www.android.com/)
[![Arduino](https://img.shields.io/badge/Arduino-00878F?style=for-the-badge&logo=arduino&logoColor=white)](https://www.arduino.cc/)
[![Bootstrap](https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white)](https://getbootstrap.com)
[![C](https://img.shields.io/badge/C-A8B9CC?style=for-the-badge&logo=c&logoColor=ffffff)](https://en.cppreference.com/w/c/language)
[![C++](https://img.shields.io/badge/C%2B%2B-00599C?style=for-the-badge&logo=cplusplus)](https://cplusplus.com/)
[![C#](https://img.shields.io/badge/C%23-239120?style=for-the-badge&logo=csharp)](https://dotnet.microsoft.com/)
[![CSS](https://img.shields.io/badge/CSS-1572B6?style=for-the-badge&logo=css3)](https://www.w3.org/Style/CSS/Overview.en.html)
[![Django](https://img.shields.io/badge/django-0C4B33?style=for-the-badge&logo=django)](https://www.djangoproject.com/)
[![Docker](https://img.shields.io/badge/Docker-384d54?style=for-the-badge&logo=docker)](https://docker.com/)
[![Electron](https://img.shields.io/badge/Electron-47848F?style=for-the-badge&logo=electron&logoColor=white)](https://www.electronjs.org/)
[![Firebase](https://img.shields.io/badge/Firebase-FFFFFF?style=for-the-badge&logo=firebase)](https://firebase.google.com/)
[![Go](https://img.shields.io/badge/Go-00ADD8?style=for-the-badge&logo=go&logoColor=ffffff)](https://go.dev/)
[![GraphQL](https://img.shields.io/badge/GraphQL-E10098?style=for-the-badge&logo=graphql)](https://graphql.org/)
[![HTML](https://img.shields.io/badge/HTML-FFFFFF?style=for-the-badge&logo=html5)](https://developer.mozilla.org/en-US/docs/Glossary/HTML)
[![Java](https://img.shields.io/badge/Java-5382A1?style=for-the-badge&logo=coffeescript&logoColor=F8981D)](https://java.com/)
[![JavaScript](https://img.shields.io/badge/JavaScript-000000?style=for-the-badge&logo=javascript)](https://javascript.com/)
[![JQuery](https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery)](https://jquery.com )
[![Laravel](https://img.shields.io/badge/Laravel-6C6C6C?style=for-the-badge&logo=laravel)](https://laravel.com)
[![Linux](https://img.shields.io/badge/Linux-eeeeee?style=for-the-badge&logo=linux&logoColor=000000)](https://www.linux.org/)
[![Momentum](https://img.shields.io/badge/Momentum-000000?style=for-the-badge&logo=data:image/svg%2bxml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDIwMDEwOTA0Ly9FTiIgImh0dHA6Ly93d3cudzMub3JnL1RSLzIwMDEvUkVDLVNWRy0yMDAxMDkwNC9EVEQvc3ZnMTAuZHRkIj4KPHN2ZyB2ZXJzaW9uPSIxLjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI1MHB0IiBoZWlnaHQ9IjI1MHB0IiB2aWV3Qm94PSIwIDAgMjUwIDI1MCIgIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiPgoJPHN0eWxlPgoJCUBtZWRpYSAocHJlZmVycy1jb2xvci1zY2hlbWU6IGxpZ2h0KSB7CgkJZyB7IGZpbGw6ICMwMDAwMDA7IH0KCQl9CgkJQG1lZGlhIChwcmVmZXJzLWNvbG9yLXNjaGVtZTogZGFyaykgewoJCWcgeyBmaWxsOiAjZmZmZmZmOyB9CgkJfQoJPC9zdHlsZT4KCTxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjUwKSBzY2FsZSgwLjEsLTAuMSkiPgoJCTxwYXRoIGQ9Ik0xMTY1IDIyOTkgYy0yNDUgLTE4IC00NzAgLTEyMyAtNjUxIC0zMDMgLTExNyAtMTE4IC0yMDAgLTI1MCAtMjUyIC00MDEgLTcxIC0yMDkgLTcxIC00NjggLTEgLTY3OSAzNyAtMTA4IDExOCAtMjU0IDE3OCAtMzE5IGwyNSAtMjggLTEzMiAtMTMyIC0xMzIgLTEzMiA1MiAtNTMgNTMgLTUyIDEzMiAxMzIgMTMyIDEzMiAyOCAtMjUgYzQxIC0zOCAxMDkgLTgyIDE4OCAtMTIxIDE1MyAtNzcgMjkyIC0xMDkgNDcwIC0xMDkgMjkxIDAgNTM2IDEwMSA3NDEgMzA1IDE1MyAxNTQgMjQ0IDMyNCAyODkgNTQwIDIxIDEwMyAyMSAyOTkgLTEgNDAyIC0zMiAxNTkgLTEwNSAzMjQgLTE4NyA0MjYgbC00OSA2MSAxMjIgMTIyIDEyMyAxMjMgLTIyIDMxIGMtMTIgMTYgLTM2IDQwIC01MiA1MiBsLTMxIDIyIC0xMjEgLTEyMSAtMTIyIC0xMjEgLTYwIDQ2IGMtODEgNjEgLTI2MSAxNDcgLTM2NSAxNzMgLTEwNCAyNyAtMjM1IDM3IC0zNTUgMjl6IG0yNzQgLTE2NCBjMTAyIC0yMSAyMTcgLTY4IDI5NyAtMTIxIDEwOSAtNzIgMTA2IC02NiA0OSAtMTI0IGwtNTAgLTUwIC01MCA5IGMtODMgMTYgLTE2OSAtNSAtMzA1IC03NCBsLTc1IC0zOCA0NSAtMzggYzI1IC0yMiA1MyAtNDIgNjMgLTQ1IDExIC0zIDQ1IDYgNzcgMjAgNjggMzEgMTM1IDQ1IDE3MCAzNiAyMiAtNSAyNSAtMTIgMjggLTU2IDEyIC0yMDQgLTM2MCAtNjQ1IC02NzQgLTgwMCAtMTA3IC01MyAtMTgwIC02NiAtMjA4IC0zOCAtMjcgMjcgLTE4IDExMyAyMiAxOTggMTggMzcgMjkgNzMgMjUgODAgLTE0IDI0IC03NiA5NiAtODIgOTYgLTEyIDAgLTc4IC0xMzIgLTEwMSAtMTk5IC0xNiAtNDkgLTIxIC04NCAtMTkgLTE0OCBsMyAtODQgLTQxIC00MSBjLTUwIC01MCAtNTIgLTQ5IC0xMTkgNTggLTU1IDg4IC05OCAxOTYgLTExOSAyOTQgLTE5IDkxIC0xOSAyNzkgMCAzNjkgNzIgMzQzIDM0NyA2MjEgNjg4IDY5NSA4NSAxOSAyODkgMTkgMzc2IDF6IG01NjkgLTM5MyBjNjMgLTk1IDEwNSAtMTk1IDEyOCAtMzA1IDE4IC04OSAxOCAtMjc3IC0xIC0zNjcgLTczIC0zNDYgLTM0OSAtNjIyIC02OTUgLTY5NSAtOTEgLTE5IC0yNzggLTE5IC0zNjggMCAtMTMwIDI3IC0yNzggOTUgLTM2NyAxNjkgbC0zMCAyNSA0NSA0NSBjNDQgNDUgNDUgNDYgMTEwIDQ2IDE3MyAwIDM5MiAxMjQgNjI1IDM1NSAxMzAgMTI5IDIwNCAyMjIgMjc4IDM1MCA2MyAxMDkgODkgMTg1IDk0IDI4MCBsNSA4MCA1MSA1MyBjMjggMjggNTUgNTIgNTkgNTIgNSAwIDM1IC00MCA2NiAtODh6Ii8+CgkJPHBhdGggZD0iTTc1MiAxODM5IGMtMTk1IC05NyAtOTMgLTQzMyAyMzggLTc5MSA3MSAtNzcgNzIgLTc4IDEwMCAtNjMgMTUgOCA0MiAyNiA2MCA0MSBsMzEgMjcgLTY5IDcxIGMtMTc2IDE4MSAtMjk3IDM3NCAtMzE3IDUwOCAtMTQgOTIgMzIgMTEwIDE1OCA2NSAxMjAgLTQ0IDI2OCAtMTQ3IDQwOSAtMjg3IGw4OCAtODcgMTkgMjEgYzEwIDEyIDI4IDM2IDM5IDU1IGwyMSAzNCAtMjYgMzEgYy00NyA1NyAtMTk3IDE4NiAtMjg1IDI0NyAtMTk2IDEzNCAtMzYxIDE4MCAtNDY2IDEyOHoiLz4KCQk8cGF0aCBkPSJNMTAxMiAxNDk3IGMtMjkgLTI5IC01MiAtNTggLTUyIC02NiAwIC0xMyA1NCAtOTYgNjggLTEwNSA1IC0zIDQ0IDMwIDg2IDczIGw3OCA3OSAtNTMgMzYgYy0zMCAyMCAtNTggMzYgLTY0IDM2IC02IDAgLTM0IC0yNCAtNjMgLTUzeiIvPgoJCTxwYXRoIGQ9Ik0xNjcyIDExMzkgYy00NiAtNTYgLTQ5IC02OSAtMjggLTEwMiAyMyAtMzUgNTYgLTE0MyA1NiAtMTgxIDAgLTc4IC01OSAtODMgLTIwNiAtMjAgbC04MyAzNiAtNTEgLTQ0IGMtMjcgLTI0IC01MCAtNDYgLTUwIC01MCAwIC0xNCAxODYgLTk5IDI1MiAtMTE0IDIwMCAtNDkgMzE3IDc4IDI2MyAyODYgLTE2IDYzIC03MCAxODYgLTk3IDIyMyAtMTMgMTcgLTE3IDE0IC01NiAtMzR6Ii8+Cgk8L2c+Cjwvc3ZnPgo=)](https://git.zakscode.com/ztimson/momentum)
[![MongoDB](https://img.shields.io/badge/mongodb-000000?style=for-the-badge&logo=mongodb)](https://www.mongodb.com/)
[![MySQL](https://img.shields.io/badge/MySQL-4479A1?style=for-the-badge&logo=mysql&logoColor=ffffff)](https://www.mysql.com/)
[![Nest](https://img.shields.io/badge/nestjs-E0234E?style=for-the-badge&logo=nestjs)](https://nestjs.com/)
[![.NET](https://img.shields.io/badge/.NET-512BD4?style=for-the-badge&logo=dotnet)](https://dotnet.microsoft.com/)
[![Next](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs)](https://nextjs.org/)
[![NGINX](https://img.shields.io/badge/NGINX-009639?style=for-the-badge&logo=nginx)](https://www.nginx.com/)
[![Node](https://img.shields.io/badge/Node.js-000000?style=for-the-badge&logo=nodedotjs)](https://nodejs.org/)
[![p5.js](https://img.shields.io/badge/p5.js-ed225d?style=for-the-badge&logo=p5dotjs&logoColor=white)](https://p5js.org/)
[![PHP](https://img.shields.io/badge/PHP-474A8A?style=for-the-badge&logo=php&logoColor=white)](https://www.php.net/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQl-212121?style=for-the-badge&logo=postgresql)](https://www.postgresql.org/)
[![Python](https://img.shields.io/badge/Python-FFD43B?style=for-the-badge&logo=python)](https://www.python.org/)
[![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react)](https://reactjs.org/)
[![Redis](https://img.shields.io/badge/Redis-ffffff?style=for-the-badge&logo=redis)](https://redis.com/)
[![SASS](https://img.shields.io/badge/SASS-CC6699?style=for-the-badge&logo=sass&logoColor=ffffff)](https://sass-lang.com/)
[![Shell](https://img.shields.io/badge/Shell-000000?style=for-the-badge&logo=windowsterminal&logoColor=00ff00)](https://en.wikipedia.org/wiki/Shell_script)
[![SQL Server](https://img.shields.io/badge/SQL%20Server-CC2927?style=for-the-badge&logo=microsoftsqlserver)](https://www.microsoft.com/en-ca/sql-server)
[![SQLite](https://img.shields.io/badge/SQLITE-003B57?style=for-the-badge&logo=sqlite)](https://www.sqlite.org/index.html)
[![Svelte](https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte)](https://svelte.dev/)
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
[![Windows](https://img.shields.io/badge/Windows-0078D4?style=for-the-badge&logo=windows)](https://microsoft.com/windows)
[![Vite](https://img.shields.io/badge/vite-1b1b1b?style=for-the-badge&logo=vite)](https://vitejs.dev/)
[![Vue](https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs)](https://vuejs.org/)
[![Linux](https://img.shields.io/badge/Linux-eeeeee?style=for-the-badge&logo=linux&logoColor=000000)](https://www.linux.org/)
## Setup
<details>
<details open>
<summary>
<h3 id="production" style="display: inline">
Production
<h3 id="installation" style="display: inline">
Installation
</h3>
</summary>
#### Prerequisites
- [Docker](https://docs.docker.com/install/)
- A Linux device with a Wi-Fi adapter (`wlan0` by default) — a Raspberry Pi or similar SBC
- `NetworkManager` and `iw` (installed automatically if missing)
- Root access
#### Instructions
1. Run the docker image: `docker run -p 80:80 git.zakscode.com/ztimson/template:latest`
2. Open [http://localhost](http://localhost)
1. Download & Install: `curl -sL https://git.zakscode.com/ztimson/fallback-ap/raw/branch/master/install.sh | sudo bash`
</details>
<details>
<summary>
<h3 id="development" style="display: inline">
Development
<h3 id="usage" style="display: inline">
Usage
</h3>
</summary>
#### Prerequisites
- [Node.js](https://nodejs.org/en/download)
Once installed, the service runs automatically and requires no further interaction. A few commands are available for manual control:
#### Instructions
1. Install the dependencies: `npm install`
2. Start the Angular server: `npm run start`
3. Open [http://localhost:4200](http://localhost:4200)
- `sudo ./pi-fallback-ap.sh status` — show service, connection, and access point state
- `sudo ./pi-fallback-ap.sh start` / `stop` — manually bring the access point up or down
- `sudo ./pi-fallback-ap.sh uninstall` — remove the service and access point profile
</details>
## License
Copyright © 2023 Zakary Timson | All Rights Reserved | Available under MIT Licensing
See the [license](./LICENSE) for more information.
Copyright © 2026 Zakary Timson | Available under MIT Licensing
+581
View File
@@ -0,0 +1,581 @@
#!/bin/bash
set -e
# Configuration =======================================================================================================
#
# Connection settings (SSID, passphrase, subnet, DHCP range, interface) can
# be supplied three ways, highest priority first:
# 1. Environment variables (FALLBACK_AP_*) - used for headless installs.
# 2. The persisted config file below, written by `install`.
# 3. The hard-coded defaults in this block.
#
# Everything else (timers, paths) is not meant to be reconfigured per-install
# and stays as plain constants.
CONFIG_FILE="/etc/fallback-ap.conf"
if [ -f "$CONFIG_FILE" ]; then source "$CONFIG_FILE"; fi
WLAN0_IFACE="${FALLBACK_AP_IFACE:-${SAVED_IFACE:-wlan0}}"
AP_NAME="${FALLBACK_AP_SSID:-${SAVED_SSID:-$HOSTNAME}}"
AP_PASSWORD="${FALLBACK_AP_PASSWORD:-${SAVED_PASSWORD:-abcd1234}}"
AP_ENCRYPTION="${FALLBACK_AP_ENCRYPTION:-${SAVED_ENCRYPTION:-wpa-psk}}" # none | wpa-psk | sae
FALLBACK_AP_CIDR="${FALLBACK_AP_CIDR:-${SAVED_CIDR:-10.10.10.1/24}}"
FALLBACK_AP_DHCP_START_OCTET="${FALLBACK_AP_DHCP_START_OCTET:-${SAVED_DHCP_START_OCTET:-100}}"
FALLBACK_AP_DHCP_END_OCTET="${FALLBACK_AP_DHCP_END_OCTET:-${SAVED_DHCP_END_OCTET:-250}}"
AP_CIDR="$FALLBACK_AP_CIDR"
AP_NETWORK_BASE="${AP_CIDR%%/*}" # e.g. "10.10.10.1" out of "10.10.10.1/24"
AP_NETWORK_BASE="${AP_NETWORK_BASE%.*}" # e.g. "10.10.10"
DHCP_START="${AP_NETWORK_BASE}.${FALLBACK_AP_DHCP_START_OCTET}"
DHCP_END="${AP_NETWORK_BASE}.${FALLBACK_AP_DHCP_END_OCTET}"
# How often, while the AP has no clients, to stop the AP and give
# NetworkManager a chance to reconnect to a known Wi-Fi network.
RECOVERY_CHECK_INTERVAL=60
# How long to leave the AP down during a recovery attempt before giving
# up and bringing the AP back.
RECOVERY_WINDOW=20
# Small delay after changing wlan0 state, to let things settle before
# we query it again.
STATE_SETTLE_DELAY=1
CONNECTION_NAME="Fallback AP"
SERVICE_NAME="fallback-ap"
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
STATE_DIR="/var/lib/fallback-ap"
LAST_WIFI_FILE="$STATE_DIR/last-wifi"
LOCK_FILE="$STATE_DIR/lock"
STATE_FILE="$STATE_DIR/state"
# End of Configuration ================================================================================================
# Resolve the actual location of this script.
SCRIPT_PATH="$(readlink -f "$0")"
if [ -z "$SCRIPT_PATH" ] || [ ! -f "$SCRIPT_PATH" ]; then
echo "Unable to determine script path."
exit 1
fi
log() {
echo "[$(date '+%H:%M:%S')] $*"
}
set_state() {
printf '%s\n' "$1" > "$STATE_FILE"
}
get_state() {
cat "$STATE_FILE" 2>/dev/null || echo "unknown"
}
is_ap_on() {
nmcli -t -f NAME connection show --active 2>/dev/null |
grep -Fxq "$CONNECTION_NAME"
}
is_wifi_connected() {
nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null |
awk -F: -v iface="$WLAN0_IFACE" -v ap="$CONNECTION_NAME" \
'$1 == iface && $2 == "wifi" && $3 == "connected" && $4 != ap {found=1} END {exit !found}'
}
get_wifi_connection() {
nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null |
awk -F: -v iface="$WLAN0_IFACE" \
'$1 == iface && $2 == "wifi" && $3 == "connected" {print $4; exit}'
}
remember_wifi() {
local connection
connection="$(get_wifi_connection)"
[ -z "$connection" ] && return
[ "$connection" = "$CONNECTION_NAME" ] && return
mkdir -p "$STATE_DIR"
if [ "$(cat "$LAST_WIFI_FILE" 2>/dev/null || true)" != "$connection" ]; then
log "Remembering Wi-Fi connection: $connection"
printf '%s\n' "$connection" > "$LAST_WIFI_FILE"
fi
}
get_last_wifi() {
[ -f "$LAST_WIFI_FILE" ] && cat "$LAST_WIFI_FILE"
}
get_client_count() {
iw dev "$WLAN0_IFACE" station dump 2>/dev/null |
grep -c '^Station ' || true
}
wait_for_ap_state() {
local wanted="$1"
local timeout="${2:-5}"
for ((i=0; i<timeout*10; i++)); do
if [ "$wanted" = "on" ] && is_ap_on; then
return 0
fi
if [ "$wanted" = "off" ] && ! is_ap_on; then
return 0
fi
sleep 0.1
done
return 1
}
wait_for_wifi() {
local timeout="$1"
for ((i=0; i<timeout*10; i++)); do
if is_wifi_connected; then
return 0
fi
sleep 0.1
done
return 1
}
start_ap() {
if is_ap_on; then
return 0
fi
log "Enabling AP: $AP_NAME"
nmcli connection up "$CONNECTION_NAME" ifname "$WLAN0_IFACE" >/dev/null 2>&1 || {
log "Failed to start AP"
return 1
}
if ! wait_for_ap_state on 5; then
log "AP failed to become active"
return 1
fi
sleep "$STATE_SETTLE_DELAY"
log "AP enabled"
return 0
}
stop_ap() {
if ! is_ap_on; then
return 0
fi
log "Disabling AP"
nmcli connection down "$CONNECTION_NAME" >/dev/null 2>&1 || true
if ! wait_for_ap_state off 5; then
log "AP failed to stop cleanly"
return 1
fi
sleep "$STATE_SETTLE_DELAY"
return 0
}
tick() {
local now="$1"
if is_wifi_connected; then
remember_wifi
if is_ap_on; then
stop_ap
fi
set_state "wifi"
last_attempt="$now"
return
fi
if ! is_ap_on; then
start_ap
set_state "fallback"
last_attempt="$now"
return
fi
set_state "fallback"
local clients
clients="$(get_client_count)"
if [ "$clients" -gt 0 ]; then
last_attempt="$now"
return
fi
if (( now - last_attempt >= RECOVERY_CHECK_INTERVAL )); then
last_attempt="$now"
attempt_recovery
fi
}
attempt_recovery() {
log "No AP clients, checking for known Wi-Fi (AP down for up to ${RECOVERY_WINDOW}s)"
set_state "checking"
stop_ap || true
if is_ap_on; then
log "AP did not go down, aborting recovery attempt"
start_ap
set_state "fallback"
return 1
fi
if wait_for_wifi "$RECOVERY_WINDOW"; then
sleep "$STATE_SETTLE_DELAY"
if is_wifi_connected; then
log "Wi-Fi reconnected"
remember_wifi
set_state "wifi"
return 0
fi
fi
log "No Wi-Fi available, restoring AP"
start_ap
set_state "fallback"
return 1
}
save_config() {
mkdir -p "$(dirname "$CONFIG_FILE")"
cat > "$CONFIG_FILE" <<EOF
SAVED_IFACE="$WLAN0_IFACE"
SAVED_SSID="$AP_NAME"
SAVED_PASSWORD="$AP_PASSWORD"
SAVED_ENCRYPTION="$AP_ENCRYPTION"
SAVED_CIDR="$FALLBACK_AP_CIDR"
SAVED_DHCP_START_OCTET="$FALLBACK_AP_DHCP_START_OCTET"
SAVED_DHCP_END_OCTET="$FALLBACK_AP_DHCP_END_OCTET"
EOF
chmod 600 "$CONFIG_FILE"
}
install() {
if [ "$EUID" -ne 0 ]; then
echo "Run this script as root."
exit 1
fi
log "Installing NetworkManager fallback AP"
apt-get update
apt-get install -y network-manager iw
systemctl enable NetworkManager
systemctl start NetworkManager
mkdir -p "$STATE_DIR"
if is_wifi_connected; then
remember_wifi
fi
create_connection
save_config
log "Creating recovery service"
cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=NetworkManager Fallback AP
After=NetworkManager.service
Wants=NetworkManager.service
[Service]
Type=simple
ExecStart=$SCRIPT_PATH run
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
chmod 644 "$SERVICE_FILE"
systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
systemctl restart "$SERVICE_NAME"
log "Installation complete"
log "Script: $SCRIPT_PATH"
log "Config file: $CONFIG_FILE"
log "SSID: $AP_NAME"
log "Interface: $WLAN0_IFACE"
log "Network: $AP_CIDR"
log "DHCP: $DHCP_START - $DHCP_END"
log "Encryption: $AP_ENCRYPTION"
log "Recovery check interval: ${RECOVERY_CHECK_INTERVAL}s"
log "Recovery window: ${RECOVERY_WINDOW}s"
log "Last Wi-Fi: $(get_last_wifi || echo none)"
echo
nmcli device status
}
create_connection() {
log "Creating NetworkManager AP connection..."
nmcli connection delete "$CONNECTION_NAME" >/dev/null 2>&1 || true
nmcli connection add \
type wifi \
ifname "$WLAN0_IFACE" \
con-name "$CONNECTION_NAME" \
autoconnect no \
ssid "$AP_NAME"
nmcli connection modify "$CONNECTION_NAME" \
connection.autoconnect no \
connection.autoconnect-priority -999 \
connection.autoconnect-retries 0 \
802-11-wireless.mode ap \
802-11-wireless.powersave 2 \
ipv4.method shared \
ipv4.addresses "$AP_CIDR" \
ipv4.shared-dhcp-range "$DHCP_START,$DHCP_END" \
ipv6.method disabled
case "$AP_ENCRYPTION" in
none)
nmcli connection modify "$CONNECTION_NAME" \
wifi-sec.key-mgmt none
;;
wpa-psk)
if [ "${#AP_PASSWORD}" -lt 8 ] || [ "${#AP_PASSWORD}" -gt 63 ]; then
echo "WPA-PSK password must be between 8 and 63 characters."
exit 1
fi
nmcli connection modify "$CONNECTION_NAME" \
wifi-sec.key-mgmt wpa-psk \
wifi-sec.psk "$AP_PASSWORD"
;;
sae)
if [ "${#AP_PASSWORD}" -lt 8 ]; then
echo "WPA3-SAE password must be at least 8 characters."
exit 1
fi
nmcli connection modify "$CONNECTION_NAME" \
wifi-sec.key-mgmt sae \
wifi-sec.psk "$AP_PASSWORD"
;;
*)
echo "Unsupported encryption: $AP_ENCRYPTION"
exit 1
;;
esac
}
uninstall() {
if [ "$EUID" -ne 0 ]; then
echo "Run this script as root."
exit 1
fi
log "Uninstalling fallback AP"
systemctl disable --now "$SERVICE_NAME" >/dev/null 2>&1 || true
nmcli connection down "$CONNECTION_NAME" >/dev/null 2>&1 || true
nmcli connection delete "$CONNECTION_NAME" >/dev/null 2>&1 || true
rm -f "$SERVICE_FILE"
rm -f "$CONFIG_FILE"
rm -rf "$STATE_DIR"
systemctl daemon-reload
log "Fallback AP removed"
}
start() {
mkdir -p "$STATE_DIR"
(
flock -n 9 || exit 1
start_ap
) 9>"$LOCK_FILE"
}
stop() {
mkdir -p "$STATE_DIR"
(
flock -n 9 || exit 1
stop_ap
) 9>"$LOCK_FILE"
}
status() {
echo
echo "=== Service ==="
systemctl status "$SERVICE_NAME" --no-pager || true
echo
echo "=== Devices ==="
nmcli device status
echo
echo "=== Active Connections ==="
nmcli connection show --active
echo
echo "=== Saved Wi-Fi Profiles ==="
nmcli -f NAME,TYPE,AUTOCONNECT,AUTOCONNECT-PRIORITY connection show
echo
echo "=== Fallback AP ==="
nmcli connection show "$CONNECTION_NAME" 2>/dev/null || echo "Not installed"
echo
echo "=== Config ($CONFIG_FILE) ==="
echo "SSID: $AP_NAME"
echo "Interface: $WLAN0_IFACE"
echo "Network: $AP_CIDR"
echo "DHCP: $DHCP_START - $DHCP_END"
echo
echo "=== Last Known Wi-Fi ==="
get_last_wifi || echo "None"
echo
echo "=== State ==="
get_state
echo
echo "=== AP Clients ==="
get_client_count
echo
echo "=== Script ==="
echo "$SCRIPT_PATH"
}
run() {
local last_attempt=0
local now
mkdir -p "$STATE_DIR"
log "Fallback AP service started"
log "Script: $SCRIPT_PATH"
exec 9>"$LOCK_FILE"
flock 9
if is_wifi_connected; then
remember_wifi
set_state "wifi"
log "Wi-Fi already connected"
else
log "No Wi-Fi connection, starting fallback AP"
start_ap
set_state "fallback"
fi
flock -u 9
while true; do
now="$(date +%s)"
if flock -n 9; then
tick "$now"
flock -u 9
fi
sleep 1
done
}
if [ "$EUID" -ne 0 ]; then
echo "Run this script as root."
exit 1
fi
case "${1:-}" in
install)
install
;;
uninstall)
uninstall
;;
start)
start
;;
stop)
stop
;;
status)
status
;;
run)
run
;;
*)
echo "Usage: $SCRIPT_PATH {install|uninstall|start|stop|status|run}"
exit 1
;;
esac
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
set -e
SCRIPT_URL="https://git.zakscode.com/ztimson/fallback-ap/raw/branch/master/fallback-ap.sh"
INSTALL_PATH="/usr/local/bin/fallback-ap"
if [ "$EUID" -ne 0 ]; then
echo "This installer needs to run as root, e.g.:"
echo " curl -sL https://git.zakscode.com/ztimson/fallback-ap/raw/branch/master/install.sh | sudo bash"
exit 1
fi
prompt() {
local __var="$1" __question="$2" __default="$3" __answer=""
read -r -p "$__question [$__default]: " __answer < /dev/tty || __answer=""
printf -v "$__var" '%s' "${__answer:-$__default}"
}
EXISTING_CONFIG="/etc/fallback-ap.conf"
if [ -f "$EXISTING_CONFIG" ]; then
# shellcheck disable=SC1090
source "$EXISTING_CONFIG"
fi
DEFAULT_SSID="${SAVED_SSID:-$(hostname)}"
DEFAULT_PASSWORD="${SAVED_PASSWORD:-abcd1234}"
DEFAULT_CIDR="${SAVED_CIDR:-10.10.10.1/24}"
DEFAULT_DHCP_START_OCTET="${SAVED_DHCP_START_OCTET:-100}"
DEFAULT_DHCP_END_OCTET="${SAVED_DHCP_END_OCTET:-250}"
[ -z "${FALLBACK_AP_SSID:-}" ] && prompt FALLBACK_AP_SSID "Access point SSID" "$DEFAULT_SSID"
[ -z "${FALLBACK_AP_PASSWORD:-}" ] && prompt FALLBACK_AP_PASSWORD "Access point passphrase" "$DEFAULT_PASSWORD"
[ -z "${FALLBACK_AP_CIDR:-}" ] && prompt FALLBACK_AP_CIDR "Access point subnet (CIDR)" "$DEFAULT_CIDR"
[ -z "${FALLBACK_AP_DHCP_START_OCTET:-}" ] && prompt FALLBACK_AP_DHCP_START_OCTET "DHCP range start (last octet)" "$DEFAULT_DHCP_START_OCTET"
[ -z "${FALLBACK_AP_DHCP_END_OCTET:-}" ] && prompt FALLBACK_AP_DHCP_END_OCTET "DHCP range end (last octet)" "$DEFAULT_DHCP_END_OCTET"
export FALLBACK_AP_SSID FALLBACK_AP_PASSWORD FALLBACK_AP_CIDR FALLBACK_AP_DHCP_START_OCTET FALLBACK_AP_DHCP_END_OCTET
echo "Downloading fallback-ap..."
curl -fsSL "$SCRIPT_URL" -o "$INSTALL_PATH"
chmod +x "$INSTALL_PATH"
echo "Running installer..."
"$INSTALL_PATH" install