diff --git a/404.html b/404.html new file mode 100644 index 0000000..5ded10a --- /dev/null +++ b/404.html @@ -0,0 +1,174 @@ + + + + + + + + My Docs + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • +
  • +
  • +
+
+
+
+
+ + +

404

+ +

Page not found

+ + +
+
+ +
+
+ +
+ +
+ +
+ + + + + +
+ + + + + + + + + diff --git a/docs/CNAME b/CNAME similarity index 100% rename from docs/CNAME rename to CNAME diff --git a/docs/assets/cable-crimping/0-boots.jpg b/assets/cable-crimping/0-boots.jpg similarity index 100% rename from docs/assets/cable-crimping/0-boots.jpg rename to assets/cable-crimping/0-boots.jpg diff --git a/docs/assets/cable-crimping/1-strip.png b/assets/cable-crimping/1-strip.png similarity index 100% rename from docs/assets/cable-crimping/1-strip.png rename to assets/cable-crimping/1-strip.png diff --git a/docs/assets/cable-crimping/2.1-untwist.png b/assets/cable-crimping/2.1-untwist.png similarity index 100% rename from docs/assets/cable-crimping/2.1-untwist.png rename to assets/cable-crimping/2.1-untwist.png diff --git a/docs/assets/cable-crimping/2.3-color-diagram.png b/assets/cable-crimping/2.3-color-diagram.png similarity index 100% rename from docs/assets/cable-crimping/2.3-color-diagram.png rename to assets/cable-crimping/2.3-color-diagram.png diff --git a/docs/assets/cable-crimping/2.3-ordered.png b/assets/cable-crimping/2.3-ordered.png similarity index 100% rename from docs/assets/cable-crimping/2.3-ordered.png rename to assets/cable-crimping/2.3-ordered.png diff --git a/docs/assets/cable-crimping/2.4-trim.png b/assets/cable-crimping/2.4-trim.png similarity index 100% rename from docs/assets/cable-crimping/2.4-trim.png rename to assets/cable-crimping/2.4-trim.png diff --git a/docs/assets/cable-crimping/3-diagram.png b/assets/cable-crimping/3-diagram.png similarity index 100% rename from docs/assets/cable-crimping/3-diagram.png rename to assets/cable-crimping/3-diagram.png diff --git a/docs/assets/cable-crimping/4-crimp.png b/assets/cable-crimping/4-crimp.png similarity index 100% rename from docs/assets/cable-crimping/4-crimp.png rename to assets/cable-crimping/4-crimp.png diff --git a/docs/assets/cable-crimping/5-test.png b/assets/cable-crimping/5-test.png similarity index 100% rename from docs/assets/cable-crimping/5-test.png rename to assets/cable-crimping/5-test.png diff --git a/docs/assets/cable-crimping/kit-cable-stripper.jpg b/assets/cable-crimping/kit-cable-stripper.jpg similarity index 100% rename from docs/assets/cable-crimping/kit-cable-stripper.jpg rename to assets/cable-crimping/kit-cable-stripper.jpg diff --git a/docs/assets/cable-crimping/kit-cable-tester.jpg b/assets/cable-crimping/kit-cable-tester.jpg similarity index 100% rename from docs/assets/cable-crimping/kit-cable-tester.jpg rename to assets/cable-crimping/kit-cable-tester.jpg diff --git a/docs/assets/cable-crimping/kit-crimping-tool.jpg b/assets/cable-crimping/kit-crimping-tool.jpg similarity index 100% rename from docs/assets/cable-crimping/kit-crimping-tool.jpg rename to assets/cable-crimping/kit-crimping-tool.jpg diff --git a/docs/assets/cable-crimping/kit-rj45-boot.jpg b/assets/cable-crimping/kit-rj45-boot.jpg similarity index 100% rename from docs/assets/cable-crimping/kit-rj45-boot.jpg rename to assets/cable-crimping/kit-rj45-boot.jpg diff --git a/docs/assets/cable-crimping/kit-rj45-connector.jpg b/assets/cable-crimping/kit-rj45-connector.jpg similarity index 100% rename from docs/assets/cable-crimping/kit-rj45-connector.jpg rename to assets/cable-crimping/kit-rj45-connector.jpg diff --git a/docs/assets/images/suzallo.jpg b/assets/images/suzallo.jpg similarity index 100% rename from docs/assets/images/suzallo.jpg rename to assets/images/suzallo.jpg diff --git a/community/code-of-conduct/index.html b/community/code-of-conduct/index.html new file mode 100644 index 0000000..46dacdb --- /dev/null +++ b/community/code-of-conduct/index.html @@ -0,0 +1,330 @@ + + + + + + + + Code of Conduct - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Code of Conduct

+

The Seattle Community Network (SCN) is dedicated to fostering an environment in which everyone can participate in our meetups, installs, online spaces, and any other community event.

+

We believe that diversity in our community is critical and should be celebrated. We welcome everyone of any race, age, gender, nationality, gender identity and expression, sexual orientation, disability, physical appearance, body size, religion, education, and skill level. The SCN community and experience often extends outside those spaces. Members meet in person to collaborate on projects, attend related meetups or conferences together, and communicate on social media. Abusive or unwelcoming behavior between SCN Members still has a profound impact on individuals and on the community when it happens beyond our events and online forums. We will use our discretion when deciding whether to enforce this code of conduct after reports of such behavior happening outside of our spaces, taking into account the impact on the individual Members involved as well as the impact on the community at large.

+

Types of Behavior

+

Encouraged Behavior

+

All participants are expected to: be considerate, respectful, and collaborative. Specifically we expect participants to:

+
    +
  • Demonstrate empathy, kindness, and patience toward other people
  • +
  • Assume the best intentions from others
  • +
  • Be respectful of differing opinions, viewpoints, and experiences
  • +
  • Give and gracefully accept constructive feedback
  • +
  • Accept responsibility and apologize to those affected by our mistakes, and learn from the experience
  • +
  • Focus on what is best not just for us as individuals, but for the overall community
  • +
+

Unacceptable Behavior

+

The following types of behavior are unacceptable at SCN, both online and in-person, and constitute code of conduct violations.

+

Abusive Behavior

+
    +
  • Harassment
      +
    • Offensive verbal comments related to gender, sexual orientation, disability, physical appearance, body size, race, nationality, immigration status, language, religion, or education level
    • +
    • Sexual images in public spaces, unwelcome sexual or romantic attention, and inappropriate physical contact.
    • +
    +
  • +
  • Threats
      +
    • Threating someone physically or verbally.
    • +
    • For example, threatening to publicize sensitive information about someone's personal life
    • +
    +
  • +
  • Hacking
      +
    • Any kind of malicious or harmful behavior towards other network users and their devices/data/property, or towards the network and its equipment or normal functioning.
    • +
    • For example, DDOS attacks or unauthorized remote access.
    • +
    +
  • +
+

Unwelcoming Behavior

+
    +
  • Blatant -isms
      +
    • Saying things that are explicitly racist, sexist, homophobic, etc.
    • +
    • For example, arguing that some people are less intelligent because of their gender, race or religion.
    • +
    • Subtle -isms and small mistakes made in conversation are not code of conduct violations. However, repeating something after it has been pointed out to you that you made a member feel unwelcome, broke a social rule or antagonizing or arguing with someone who has pointed out your subtle -ism is considered unwelcoming behavior and is not allowed in SCN.
    • +
    +
  • +
  • Maliciousness towards other members
      +
    • Deliberately attempting to make others feel bad, name-calling, singling out others for derision or exclusion.
    • +
    • For example, telling somone they're not technical enough or that they don't belong in SCN.
    • +
    +
  • +
  • Being especially unpleasant
      +
    • For example, if we've received reports from multiple members of annoying, rude, or especially distracting behavior.
    • +
    • For example, repeatedly engaging in bad faith arguments, talking down to people, or excluding people from participation.
    • +
    +
  • +
+

Reporting

+

Please report code of conduct violations either to the event organizer, by emailing lcl@seattlecommunitynetwork.org or by alerting moderators on Discord with the @moderators group tag.

+

All of our moderators are volunteers, and will response with their best effort. However, if you provide your name, or contact info we promise to respond within two business days.

+

How to Report

+

In your report, please include:

+
    +
  • Subject line
      +
    • "[SCN Code of Conduct Violation Report]" followed by descriptive subject
    • +
    +
  • +
  • Your name
      +
    • This is incredibly helpful for us to be able to follow up with you, and ask questions to better understand the situation.
    • +
    • You are welcome to report anonymously. Please only use this option if you really need to, and know that we might not be able to take action without knowing who you are.
    • +
    • In any case, provide an email address so we can correspond with you about the report.
    • +
    +
  • +
  • A detailed description of what happened
      +
    • If the violation happened online, please link to or send us the relevant text.
    • +
    • If the violation happened in person, please detail exactly what the other person said or did. In order to take action, we need to know the concrete actions that someone took.
    • +
    +
  • +
  • Where and when the incident happened
  • +
  • Any other relevant context. Do you have examples of a pattern of similar behavior? Do you have a relationship with this person outside of SCN?
  • +
  • If/how you've already responded - this lets us know the current state of the situation.
  • +
+

Why to Report

+
    +
  • You are responsible for making SCN a safe and comfortable space for everyone. Everyone in our community shares this responsibility. Our volunteer Moderators are not around all the time, so we cannot enforce the code of conduct without your help.
  • +
  • The consequences for SCN of not reporting bad behavior outweigh the consequences for one person reporting it. We sometimes hear “I don’t want X person to meet consequences because I told someone about their bad behavior.” Consider the impact on everyone else at SCN of letting their behavior continue unchecked.
  • +
  • SCN only works as a self-directed, community-driven effort because of shared trust between members. Reporting code of conduct violations helps us identify when this trust is broken, to prevent that from happening in the future.
  • +
+

Enforcement Guidelines

+

Moderators will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct.

+

Correction

+

Community Impact: Unwelcoming behavior. Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.

+

Consequence: A private, written warning from moderators, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public or private apology may be requested in the venue where the behavior took place, for example on Discord or at the event.

+

Warning

+

Community Impact: A violation through a single incident of abusive behavior or series of unwelcoming behaviors.

+

Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.

+

Temporary Ban

+

Community Impact: A serious violation of community standards, including sustained unwelcoming behavior.

+

Consequence: A temporary ban from any sort of participation, interaction, or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.

+

Permanent Ban

+

Community Impact: Demonstrating a pattern of violation of community standards, including sustained unwelcoming behavior, harassment or threatening of an individual, or aggression toward or disparagement of classes of individuals.

+

Consequence: A permanent ban from any sort of participation, or public interaction within the community.

+

Discussion

+

If you have questions about any aspect of the code of conduct, or want to propose amending it, please contact the @moderators group, or drop in to the public #governance channel on Discord.

+

Attribution

+

Taken almost verbatim from the NYC Mesh Code of Conduct.

+

NYC Mesh CoC Attribution:

+

Most of this is from the Recurse Center CoC. Other parts are from Strange Loop(community goals), Contributor Covenant (portion of the community goals, encouraged behaviors, enforcement guidelines), and the Toronto Mesh/Geek Feminism (guidelines for moderators).

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/community/community-networking-toolkit/index.html b/community/community-networking-toolkit/index.html new file mode 100644 index 0000000..d86ed11 --- /dev/null +++ b/community/community-networking-toolkit/index.html @@ -0,0 +1,278 @@ + + + + + + + + Community Networking Toolkit - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Community Networking Toolkit

+

This is a toolkit/resource guide for anyone interested in starting up their own community network.

+ +

Here is a spreadsheet containing a bunch of tools and equipment that you should bring to network deployments. This was created by Esther Jang who has a lot of practical experience deploying sites. This spreadsheet is color-coded into different categories based on how important they are to have!

+

Ethernet Crimping

+

Ethernet crimping is an important skill for network site installations. The length of an ethernet cable connection is only truly known once you're on site so it is useful to be able to quickly cut ethernet cable to the desired length and crimp it.

+ +

Internet Society (ISOC) Materials

+ +

All-aspects Community Networking Guides

+
    +
  • AlterMundi (Spanish Language)
  • +
  • NYC Mesh - How to start a community network
  • +
  • NYC Mesh Docs
  • +
  • Telecommunications Reclaimed - Linked from NYC Mesh guide
  • +
  • Start your own ISP
      +
    • This site is dedicated to helping you start your own Internet Service Provider. Specifically this guide is about building a Wireless ISP (WISP).
    • +
    +
  • +
  • Commotion Construction Kit
      +
    • The Commotion Construction Kit is a set of documentation tools that the Open Technology Institute has used in workshops around the world and at home. It is a “do it ourselves” guide to building community wireless networks.
    • +
    +
  • +
  • Neighborhood Network Construction Kit
      +
    • The Neighborhood Network Construction Kit is a set of documentation tools that the Open Technology Institute has used in workshops around the world and at home. It is a “do it ourselves” guide to building community wireless networks. Many of these activities were first released as part of the Commotion project.
    • +
    +
  • +
  • Community Technology Field Guide
      +
    • A collective resource for digital stewardship, digital justice and community infrastructure. These resources emphasize self-governance, participatory learning, collaborative design and sustainability.
    • +
    +
  • +
  • Building Broadband Commons - Tools for Planners and Communities
  • +
  • Next Century City - Becoming Broadband Ready, a Toolkit for Communities
  • +
  • Wireless Networking in the Developing World - Online Book
      +
    • Wireless Networking in the Developing World is a free book about designing, implementing, and maintaining low-cost wireless networks.
    • +
    +
  • +
  • Community Networks in Comics
      +
    • It is not easy to explain the concepts behind community networks, both the technical characteristics of radio frequency networks and the social and human aspects of community technologies.
    • +
    • Teaching a workshop for popular groups using colonizing terms and methodologies can increase the existing barrier between people and a technology that was not created for their interests.
    • +
    • With this in mind, images and analogies are powerful tools to make it easier to explain a technical term or an idea. We reject the premise that to do so would in any way underestimate people’s ability to understand technical matters.
    • +
    +
  • +
  • Building Consentful Tech Zine
      +
    • In 2017, Una Lee, Dann Toliver, and their design firm And Also Too published the Building Consentful Tech Zine as a provocation on how to think about and design for consentfulness.
    • +
    • This framing really resonated with our group, so we expanded it into a project where we prototype what this looks like in practice (and learned some interaction design methodologies on the way)!
    • +
    +
  • +
  • Report on Digital Skill Sets for Diverse Users
      +
    • The City of Seattle, in partnership with Technology and Social Change Group (TASCHA), developed a set of Digital Equity Indicators that helps measure Seattle’s progress in meeting the initiative's goals.
        +
      • What digital skills do existing frameworks and curricula cover?
      • +
      • What digital skills should the City and partners recommend to digital skill instructors to teach and promote?
      • +
      • Do these resources have corresponding assessments to help assess individuals’ digital skill abilities?
      • +
      +
    • +
    +
  • +
  • Worksheet for Digital Skills for Diverse Users
      +
    • A list of 74 skills identified by TASCHA (see above resource) to include in digital equity curriculums
    • +
    +
  • +
  • Report on current state of Detroit Community Network
      +
    • In this case study, we focus on Detroit and the predominantly Black and lower-income neighborhood of the North End as an example of innovative, community-scale projects that are locally generated.
    • +
    +
  • +
  • New Community Networks by Douglas Schuler
      +
    • Online book about building and socially sustaining community networks, based on Doug Schuler's experiences with the Seattle Community Network project in the 90's (1996)
    • +
    +
  • +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/community/join/index.html b/community/join/index.html new file mode 100644 index 0000000..eca2289 --- /dev/null +++ b/community/join/index.html @@ -0,0 +1,249 @@ + + + + + + + + Join Us - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Join Us

+

Seattle Community Network has a place for everyone. Whether you'd like to join +to get free Internet, get involved to help out your community, learn some skills +so that you can get a job, or all of the above!

+

PRO TIP Do everything on this page! Our community is spread out in a lot of different places, so doing everything is the best way to make sure you don't miss out.

+

Join our Discord!

+

Discord is a messaging platform that we use to stay in touch +with each other, organize our different teams, and update everyone on last minute things.

+

This is a MUST do if you don't want to get left out!

+

Follow these steps to join our Discord server:

+
    +
  1. Click the invite link to enter the Discord server.
  2. +
  3. Introduce yourself on the #introductions channel and friend request one of the moderators so they can chat with you (you can write @moderators in your message to get their attention). They will need to add a role for you as a "member" so you can stay on the server before you log out, otherwise you will have to be invited and join again.
  4. +
  5. Install Discord on your computer, Android, or iOS device to always stay up-to-date.
  6. +
+

Chat with someone!

+

The best way to get involved with the network is to find someone that can direct you! Most of our work happens in teams. Here's how to join a team.

+

Subscribe to our Google calendar!

+

On our Google calendar we post regular occurring meetings and any impromptu events like social events, emergency repairs, site visits, etc. +This is one of the only places to find out about the meetings our various teams are having!

+

Join using one of these options:

+
    +
  • Use this link that should prompt you to add the calendar to your Google account
  • +
  • Use this ICS file to manually add the calendar via the iCalendar format.
  • +
  • View the calendar here
  • +
+

Follow our social media!

+

Our social media team makes posts about upcoming meetings, social events, and they also occasionally +make educational posts!

+

Don't miss out and follow us on Instagram, Facebook, and Twitter.

+

Visit our Space!

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/community/mission-vision-values/index.html b/community/mission-vision-values/index.html new file mode 100644 index 0000000..c26ce0b --- /dev/null +++ b/community/mission-vision-values/index.html @@ -0,0 +1,209 @@ + + + + + + + + Mission, Vision, Values - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Mission, Vision, and Values

+

Mission

+

LCL seeks to democratize knowledge, skills, and resources to enable people to establish and run their own local, community-centered, and community-owned Internet access networks and digital infrastructure.

+

Vision

+

We envision a world where no one is excluded from access to the Internet, and where anyone can achieve the expertise and capability to bring communications infrastructure to their community and improve their quality of life.

+

Values

+

We value the ability to access the Internet and all public information and digital resources therein as a human right. +- Digital privacy of our users and partner organizations +- Collaboration, especially with the communities and organizations we work with +- Care, consideration, allyship, and peer mentorship between individuals within our organization +- Education, sharing, and capacity-building- emphasize teaching and dissemination of information and skills +- Openness, transparency, and accountability of our organization and its processes +- Democratization and inclusiveness of decision processes among stakeholders +- Long-term sustainability of our technology deployments and community structures +- Equity in planning for resource allocation, programming, and contribution

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/community/partners/index.html b/community/partners/index.html new file mode 100644 index 0000000..810ae82 --- /dev/null +++ b/community/partners/index.html @@ -0,0 +1,204 @@ + + + + + + + + Our Partners - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Our Partners

+
    +
  • Althea
  • +
  • API Chaya (WiFi is a Lifeline)
  • +
  • Black Brilliance Research Project
  • +
  • Breakfast Group
  • +
  • Cham Refugees Community
  • +
  • City of Seattle IT
  • +
  • Filipino Community of Seattle
  • +
  • King County Library System
  • +
  • Oromo Cultural Center
  • +
  • Seattle Public Schools
  • +
  • Tacoma Cooperative Network
  • +
  • Tacoma Public Library
  • +
  • University of Washington
  • +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/community/teams/index.html b/community/teams/index.html new file mode 100644 index 0000000..ebf45f7 --- /dev/null +++ b/community/teams/index.html @@ -0,0 +1,249 @@ + + + + + + + + Join a Team - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Join a Team

+

As a volunteer-run community network, we are always in need of extra hands. +If you're interested in helping out, check out what our teams are working on here!

+

Feel free to have informational meetings with the respective team leads to learn more about how you can help. There's no commitment required and we welcome you to take on as much work as you have the capacity for.

+

Outreach

+

Our outreach team is responsible for finding new site host partners, finding users, and maintaining communications with our current partners. Message the #outreach channel in Discord and contact Esther Jang to learn more about how you can get involved.

+

This is the perfect team for you if any of the following apply to you:

+
    +
  • Experience with community organizing
  • +
  • Have community connections in the Greater Seattle Area
  • +
  • Have cultural humility and experience partnering with marginalized communities
  • +
  • Have the ability to translate and/or interpret into non-English languages common in the Seattle area such as Spanish, Vietnamese, Somali, Oromo, Khmer, and more.
  • +
+

As of July 2021, the primary objective of the outreach team is to get connected with users for our network sites that fit any of the following criteria:

+
    +
  • Unemployed
  • +
  • Seniors
  • +
  • Housing-unstable or houseless
  • +
  • Non-English speaking
  • +
+

Social Media

+

Our social media team is in charge of our various accounts on Instagram, Facebook, and Twitter. They also develop branding and marketing materials for our various projects. Message the #social-media channel in Discord and contact Firn Tieanklin to learn more about how you can get involved.

+

You should join this team if you have experience in or are interested in practicing any of the following skills/technologies: +- Canva +- Design +- Marketing +- Photography/Videography

+

Web Development

+

Our web development team is working on redesigning and developing our new website. Message the #website channel in Discord to learn more about how you can get involved.

+

You should join this team if you have experience in or are interested in learning any of the following skills/technologies:

+
    +
  • HTML/CSS
  • +
  • Javascript
  • +
  • Bootstrap
  • +
  • React
  • +
  • Redux
  • +
  • Web Design (Using Figma)
  • +
+

Mobile App Development

+

Our mobile app development team is currently developing an Android app to record cell network performance metrics during our field tests.

+

Message the #measurement channel in Discord and contact Zhennan Zhou to learn more about how you can get involved.

+

You should join this team if you have experience in or are interested in learning any of the following skills/technologies:

+
    +
  • Android app development
  • +
  • iOS app development
  • +
  • Java
  • +
  • Object oriented programming
  • +
  • Open source development, Git, and GitHub
  • +
+

Education

+

Our education team plays a core role in our community networks as they enable our networks to be community-owned and operated. The education team is responsible for developing educational materials, running workshops, and teaching our Digital Steward cohorts. Message the #digital-stewards channel in Discord and contact Esther Jang to learn more about how you can get involved.

+

You should join this team if you have experience in or are interested in learning any of the following skills/technologies:

+
    +
  • Teaching
  • +
  • Curriculum development
  • +
  • Computer networks, LTE networks, and community networks
  • +
  • Linux
  • +
  • Community organizing
  • +
+

Fundraising

+

Our fundraising team is currently setting up a crowdfunding platform to raise money for various expenses that this project requires. Our fundraising team is also working on applying for various community grants and research grants. Message the #funding channel in Discord and contact Esther Jang to learn more about how you can get involved.

+ +

Local Connectivity Lab, the nonprofit organizing that is incubating this project, often runs into accounting and legal challenges. If you are willing to provide pro bono services to benefit the community network, please contact Esther Jang at lcl@seattlecommunitynetwork.org.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/community/tech-help/index.html b/community/tech-help/index.html new file mode 100644 index 0000000..e98cea9 --- /dev/null +++ b/community/tech-help/index.html @@ -0,0 +1,239 @@ + + + + + + + + Community Tech Help - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Community Tech Help

+

There are several ways to get community-based tech support from the SCN community, such as the Help Desk. +You can use any of these methods to request Internet service from us, request general technology or computer help, or contact us about any other topic.

+

Join our Discord!

+

The FASTEST way to get support will be to join the #support channel on our Discord, a messaging platform that we use to organize.

+

Follow these steps to join Discord:

+
    +
  1. Join our Discord server via the invite link. You will need to log in or create an account to join, and a moderator will need to assign you a role before you are allowed to join permanently.
  2. +
  3. Install Discord for your computer, Android, or iOS device to stay up-to-date on conversations.
  4. +
  5. When posting to the #support channel, describe your question or problem in as much detail as you can. Someone from the community will most likely respond within a few hours.
  6. +
  7. If you would like to get to know the community, introduce yourself in the #introductions channel! How did you hear about SCN and why are you interested? +(More complete instructions can be found here).
  8. +
+

Community-Run Help Desk

+

The Seattle Community Network organizes a community-run tech help desk supported by the Black Brilliance Research Project's (BBR's) Digital Stewards Program and the Filipino Community of Seattle (FCS). Our current in-person help desk hours are on Fridays at 3-5 pm starting on 2/18/2022, located in the Filipino Community Village Integrated Learning Center (ILC).

+

Filipino Community Village Integrated Learning Center address: +5727 37th Ave S, Seattle, WA 98118

+

Our help desk is best-effort and mainly volunteer-based, so our virtual hours availability may be highly variable. +Please check our available hours and sign up for an appointment slot using the calendar link below if you can (or contact us another way if that doesn't work), as it helps the volunteers plan for our time. +However, you may also drop in during our scheduled calendar hours without an appointment.

+
    +
  • Our Help Desk Calendar: You can sign up for an appointment slot for any staffed volunteer hours indicated on the calendar.
  • +
  • Phone Number (Voicemail-only except during staffed Community Tech Help hours): (253) 655-7221
      +
    • You may also send text messages to this number, which will be checked during staffed hours.
    • +
    • Inquiries about getting connected to our Internet service can also be sent here.
    • +
    +
  • +
  • Email address for general Tech Support: support@seattlecommunitynetwork.org
  • +
  • Email address for SCN Internet service-related support: help@seattlecommunitynetwork.org
  • +
+

Help Desk Volunteers

+

We are actively recruiting more volunteers to help us run the help desk virtually, whenever and wherever that you are available. +Join our team by simply signing up on this interest form, we need you!

+

Please let us know if you have additional questions or concerns in the #support channel on our Discord.

+

Documentation

+

As always, please do not hesitate to consult our docs for any information, or submit an issue on the docs site github if there is information missing that you would like to see. +Also feel free to message the Discord for the same purpose.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/contribute/contribute/index.html b/contribute/contribute/index.html new file mode 100644 index 0000000..ceca353 --- /dev/null +++ b/contribute/contribute/index.html @@ -0,0 +1,247 @@ + + + + + + + + Contribute to SCN Docs - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Contribute to SCN Docs

+

Looking to help?

+

If you wanna share resources and help improve our docs, this page will get you started! +Our docs are designed so that anyone can contribute. If this page isn't enough, contact one of +us and we'll be able to help you!

+

Our documentation uses MkDocs ReadTheDocs theme (links at the bottom of the page)

+

Editing process

+

To edit this documentation you should:

+
    +
  1. Get your own copy of the repo
  2. +
  3. Modify the documentation in your own repo (for more information see section on Local Development)
  4. +
  5. Submit a pull request
  6. +
  7. Wait for someone to review and accept the request
  8. +
+

The rest of this page will explain all the details you need to know about the directory structure, markdown, and other quirks for editing this documentation. Make sure to read everything!

+

Markdown files

+

All our documentation is stored in 'Markdown' files so that they can be easily +modified and changed without heavy technical knowledge.

+

Markdown Editors

+
    +
  • +

    A nice and simple online editor is StackEdit which will let you type in markdown and see what it would look like in realtime in split-screen.

    +
  • +
  • +

    Another option is HackMD which has the same features as StackEdit +but it also allows you to connect to your own GitHub repo and pull/push.

    +
      +
    • This is a nice option if you are uncomfortable with using Git from the command line.
    • +
    +
  • +
+

Local Development

+

Using MkDocs

+

MkDocs is pretty simple, just install it through pip then you can run mkdocs serve to locally view the website it will generate.

+

Documentation Directory Structure

+

Each directory has a .site file that declares the order each file/folder in that directory will show up. +Children pages are implicit in the directory structure

+

Static Files

+

If you need static files for any of your pages, you should put them in the assets folder within the top level docs folder

+

For example, for the cable-crimping page, the images for the tutorial are located in the "assets/cable-crimping" folder. These images can then be referenced relatively with the following standard markdown image syntax:

+
![RJ45 Crimping Tool](../assets/cable-crimping/kit-crimping-tool.jpg)
+
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + +
+ + + + + + + + + diff --git a/css/fonts/Roboto-Slab-Bold.woff b/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/css/fonts/Roboto-Slab-Bold.woff2 b/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 0000000..7059e23 Binary files /dev/null and b/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/css/fonts/Roboto-Slab-Regular.woff b/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 0000000..f815f63 Binary files /dev/null and b/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/css/fonts/Roboto-Slab-Regular.woff2 b/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/css/fonts/fontawesome-webfont.eot b/css/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/css/fonts/fontawesome-webfont.eot differ diff --git a/css/fonts/fontawesome-webfont.svg b/css/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/css/fonts/fontawesome-webfont.ttf b/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/css/fonts/fontawesome-webfont.ttf differ diff --git a/css/fonts/fontawesome-webfont.woff b/css/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/css/fonts/fontawesome-webfont.woff differ diff --git a/css/fonts/fontawesome-webfont.woff2 b/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/css/fonts/fontawesome-webfont.woff2 differ diff --git a/css/fonts/lato-bold-italic.woff b/css/fonts/lato-bold-italic.woff new file mode 100644 index 0000000..88ad05b Binary files /dev/null and b/css/fonts/lato-bold-italic.woff differ diff --git a/css/fonts/lato-bold-italic.woff2 b/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 0000000..c4e3d80 Binary files /dev/null and b/css/fonts/lato-bold-italic.woff2 differ diff --git a/css/fonts/lato-bold.woff b/css/fonts/lato-bold.woff new file mode 100644 index 0000000..c6dff51 Binary files /dev/null and b/css/fonts/lato-bold.woff differ diff --git a/css/fonts/lato-bold.woff2 b/css/fonts/lato-bold.woff2 new file mode 100644 index 0000000..bb19504 Binary files /dev/null and b/css/fonts/lato-bold.woff2 differ diff --git a/css/fonts/lato-normal-italic.woff b/css/fonts/lato-normal-italic.woff new file mode 100644 index 0000000..76114bc Binary files /dev/null and b/css/fonts/lato-normal-italic.woff differ diff --git a/css/fonts/lato-normal-italic.woff2 b/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 0000000..3404f37 Binary files /dev/null and b/css/fonts/lato-normal-italic.woff2 differ diff --git a/css/fonts/lato-normal.woff b/css/fonts/lato-normal.woff new file mode 100644 index 0000000..ae1307f Binary files /dev/null and b/css/fonts/lato-normal.woff differ diff --git a/css/fonts/lato-normal.woff2 b/css/fonts/lato-normal.woff2 new file mode 100644 index 0000000..3bf9843 Binary files /dev/null and b/css/fonts/lato-normal.woff2 differ diff --git a/css/theme.css b/css/theme.css new file mode 100644 index 0000000..ad77300 --- /dev/null +++ b/css/theme.css @@ -0,0 +1,13 @@ +/* + * This file is copied from the upstream ReadTheDocs Sphinx + * theme. To aid upgradability this file should *not* be edited. + * modifications we need should be included in theme_extra.css. + * + * https://github.com/readthedocs/sphinx_rtd_theme + */ + + /* sphinx_rtd_theme version 1.2.0 | MIT license */ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} diff --git a/css/theme_extra.css b/css/theme_extra.css new file mode 100644 index 0000000..ab0631a --- /dev/null +++ b/css/theme_extra.css @@ -0,0 +1,197 @@ +/* + * Wrap inline code samples otherwise they shoot of the side and + * can't be read at all. + * + * https://github.com/mkdocs/mkdocs/issues/313 + * https://github.com/mkdocs/mkdocs/issues/233 + * https://github.com/mkdocs/mkdocs/issues/834 + */ +.rst-content code { + white-space: pre-wrap; + word-wrap: break-word; + padding: 2px 5px; +} + +/** + * Make code blocks display as blocks and give them the appropriate + * font size and padding. + * + * https://github.com/mkdocs/mkdocs/issues/855 + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/issues/233 + */ +.rst-content pre code { + white-space: pre; + word-wrap: normal; + display: block; + padding: 12px; + font-size: 12px; +} + +/** + * Fix code colors + * + * https://github.com/mkdocs/mkdocs/issues/2027 + */ +.rst-content code { + color: #E74C3C; +} + +.rst-content pre code { + color: #000; + background: #f8f8f8; +} + +/* + * Fix link colors when the link text is inline code. + * + * https://github.com/mkdocs/mkdocs/issues/718 + */ +a code { + color: #2980B9; +} +a:hover code { + color: #3091d1; +} +a:visited code { + color: #9B59B6; +} + +/* + * The CSS classes from highlight.js seem to clash with the + * ReadTheDocs theme causing some code to be incorrectly made + * bold and italic. + * + * https://github.com/mkdocs/mkdocs/issues/411 + */ +pre .cs, pre .c { + font-weight: inherit; + font-style: inherit; +} + +/* + * Fix some issues with the theme and non-highlighted code + * samples. Without and highlighting styles attached the + * formatting is broken. + * + * https://github.com/mkdocs/mkdocs/issues/319 + */ +.rst-content .no-highlight { + display: block; + padding: 0.5em; + color: #333; +} + + +/* + * Additions specific to the search functionality provided by MkDocs + */ + +.search-results { + margin-top: 23px; +} + +.search-results article { + border-top: 1px solid #E1E4E5; + padding-top: 24px; +} + +.search-results article:first-child { + border-top: none; +} + +form .search-query { + width: 100%; + border-radius: 50px; + padding: 6px 12px; + border-color: #D1D4D5; +} + +/* + * Improve inline code blocks within admonitions. + * + * https://github.com/mkdocs/mkdocs/issues/656 + */ + .rst-content .admonition code { + color: #404040; + border: 1px solid #c7c9cb; + border: 1px solid rgba(0, 0, 0, 0.2); + background: #f8fbfd; + background: rgba(255, 255, 255, 0.7); +} + +/* + * Account for wide tables which go off the side. + * Override borders to avoid weirdness on narrow tables. + * + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/pull/1034 + */ +.rst-content .section .docutils { + width: 100%; + overflow: auto; + display: block; + border: none; +} + +td, th { + border: 1px solid #e1e4e5 !important; + border-collapse: collapse; +} + +/* + * Without the following amendments, the navigation in the theme will be + * slightly cut off. This is due to the fact that the .wy-nav-side has a + * padding-bottom of 2em, which must not necessarily align with the font-size of + * 90 % on the .rst-current-version container, combined with the padding of 12px + * above and below. These amendments fix this in two steps: First, make sure the + * .rst-current-version container has a fixed height of 40px, achieved using + * line-height, and then applying a padding-bottom of 40px to this container. In + * a second step, the items within that container are re-aligned using flexbox. + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ + .wy-nav-side { + padding-bottom: 40px; +} + +/* For section-index only */ +.wy-menu-vertical .current-section p { + background-color: #e3e3e3; + color: #404040; +} + +/* + * The second step of above amendment: Here we make sure the items are aligned + * correctly within the .rst-current-version container. Using flexbox, we + * achieve it in such a way that it will look like the following: + * + * [No repo_name] + * Next >> // On the first page + * << Previous Next >> // On all subsequent pages + * + * [With repo_name] + * Next >> // On the first page + * << Previous Next >> // On all subsequent pages + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ +.rst-versions .rst-current-version { + padding: 0 12px; + display: flex; + font-size: initial; + justify-content: space-between; + align-items: center; + line-height: 40px; +} + +/* + * Please note that this amendment also involves removing certain inline-styles + * from the file ./mkdocs/themes/readthedocs/versions.html. + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ +.rst-current-version span { + flex: 1; + text-align: center; +} diff --git a/docs/.pages b/docs/.pages deleted file mode 100644 index d3eb4bd..0000000 --- a/docs/.pages +++ /dev/null @@ -1,7 +0,0 @@ -nav: - - index.md - - faq - - community - - learn - - infrastructure - - contribute \ No newline at end of file diff --git a/docs/assets/.DS_Store b/docs/assets/.DS_Store deleted file mode 100644 index 8774dfc..0000000 Binary files a/docs/assets/.DS_Store and /dev/null differ diff --git a/docs/assets/cable-crimping/.DS_Store b/docs/assets/cable-crimping/.DS_Store deleted file mode 100644 index 257f179..0000000 Binary files a/docs/assets/cable-crimping/.DS_Store and /dev/null differ diff --git a/docs/community/.pages b/docs/community/.pages deleted file mode 100644 index 2c97e6b..0000000 --- a/docs/community/.pages +++ /dev/null @@ -1,8 +0,0 @@ -nav: - - join.md - - mission-vision-values.md - - code-of-conduct.md - - join.md - - partners.md - - tech-help.md - - community-networking-toolkit.md \ No newline at end of file diff --git a/docs/community/code-of-conduct.md b/docs/community/code-of-conduct.md deleted file mode 100644 index 0d9fd31..0000000 --- a/docs/community/code-of-conduct.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: Code of Conduct ---- - -# Code of Conduct - -The Seattle Community Network (SCN) is dedicated to fostering an environment in which everyone can participate in our meetups, installs, online spaces, and any other community event. - -We believe that diversity in our community is critical and should be celebrated. We welcome everyone of any race, age, gender, nationality, gender identity and expression, sexual orientation, disability, physical appearance, body size, religion, education, and skill level. The SCN community and experience often extends outside those spaces. Members meet in person to collaborate on projects, attend related meetups or conferences together, and communicate on social media. Abusive or unwelcoming behavior between SCN Members still has a profound impact on individuals and on the community when it happens beyond our events and online forums. We will use our discretion when deciding whether to enforce this code of conduct after reports of such behavior happening outside of our spaces, taking into account the impact on the individual Members involved as well as the impact on the community at large. - -## Types of Behavior - -### Encouraged Behavior - -All participants are expected to: be considerate, respectful, and collaborative. Specifically we expect participants to: - -- Demonstrate empathy, kindness, and patience toward other people -- Assume the best intentions from others -- Be respectful of differing opinions, viewpoints, and experiences -- Give and gracefully accept constructive feedback -- Accept responsibility and apologize to those affected by our mistakes, and learn from the experience -- Focus on what is best not just for us as individuals, but for the overall community - -### Unacceptable Behavior - -The following types of behavior are unacceptable at SCN, both online and in-person, and constitute code of conduct violations. - -#### Abusive Behavior - -- Harassment - - Offensive verbal comments related to gender, sexual orientation, disability, physical appearance, body size, race, nationality, immigration status, language, religion, or education level - - Sexual images in public spaces, unwelcome sexual or romantic attention, and inappropriate physical contact. -- Threats - - Threating someone physically or verbally. - - For example, threatening to publicize sensitive information about someone's personal life -- Hacking - - Any kind of malicious or harmful behavior towards other network users and their devices/data/property, or towards the network and its equipment or normal functioning. - - For example, DDOS attacks or unauthorized remote access. -#### Unwelcoming Behavior - -- Blatant -isms - - Saying things that are explicitly racist, sexist, homophobic, etc. - - For example, arguing that some people are less intelligent because of their gender, race or religion. - - [Subtle -isms](https://www.recurse.com/social-rules#no-subtle-isms) and small mistakes made in conversation are not code of conduct violations. However, repeating something after it has been pointed out to you that you made a member feel unwelcome, broke a social rule or antagonizing or arguing with someone who has pointed out your subtle -ism is considered unwelcoming behavior and is not allowed in SCN. -- Maliciousness towards other members - - Deliberately attempting to make others feel bad, name-calling, singling out others for derision or exclusion. - - For example, telling somone they're not technical enough or that they don't belong in SCN. -- Being especially unpleasant - - For example, if we've received reports from multiple members of annoying, rude, or especially distracting behavior. - - For example, repeatedly engaging in bad faith arguments, talking down to people, or excluding people from participation. - -## Reporting - -Please report code of conduct violations either to the event organizer, by emailing lcl@seattlecommunitynetwork.org or by alerting moderators on Discord with the @moderators group tag. - -All of our moderators are volunteers, and will response with their best effort. However, if you provide your name, or contact info we promise to respond within two business days. - -### How to Report - -In your report, please include: - -- Subject line - - "[SCN Code of Conduct Violation Report]" followed by descriptive subject -- Your name - - This is incredibly helpful for us to be able to follow up with you, and ask questions to better understand the situation. - - You are welcome to report anonymously. Please only use this option if you really need to, and know that we might not be able to take action without knowing who you are. - - In any case, provide an email address so we can correspond with you about the report. -- A detailed description of what happened - - If the violation happened online, please link to or send us the relevant text. - - If the violation happened in person, please detail exactly what the other person said or did. In order to take action, we need to know the concrete actions that someone took. -- Where and when the incident happened -- Any other relevant context. Do you have examples of a pattern of similar behavior? Do you have a relationship with this person outside of SCN? -- If/how you've already responded - this lets us know the current state of the situation. - -### Why to Report -- You are responsible for making SCN a safe and comfortable space for everyone. Everyone in our community shares this responsibility. Our volunteer Moderators are not around all the time, so we cannot enforce the code of conduct without your help. -- The consequences for SCN of not reporting bad behavior outweigh the consequences for one person reporting it. We sometimes hear “I don’t want X person to meet consequences because I told someone about their bad behavior.” Consider the impact on everyone else at SCN of letting their behavior continue unchecked. -- SCN only works as a self-directed, community-driven effort because of shared trust between members. Reporting code of conduct violations helps us identify when this trust is broken, to prevent that from happening in the future. - -## Enforcement Guidelines - -Moderators will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct. - -### Correction - -Community Impact: Unwelcoming behavior. Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. - -Consequence: A private, written warning from moderators, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public or private apology may be requested in the venue where the behavior took place, for example on Discord or at the event. - -### Warning - -Community Impact: A violation through a single incident of abusive behavior or series of unwelcoming behaviors. - -Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. - -### Temporary Ban - -Community Impact: A serious violation of community standards, including sustained unwelcoming behavior. - -Consequence: A temporary ban from any sort of participation, interaction, or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -### Permanent Ban - -Community Impact: Demonstrating a pattern of violation of community standards, including sustained unwelcoming behavior, harassment or threatening of an individual, or aggression toward or disparagement of classes of individuals. - -Consequence: A permanent ban from any sort of participation, or public interaction within the community. - -## Discussion - -If you have questions about any aspect of the code of conduct, or want to propose amending it, please contact the @moderators group, or drop in to the public #governance channel on Discord. - -## Attribution - -Taken almost verbatim from the [NYC Mesh Code of Conduct](https://www.nycmesh.net/coc). - -NYC Mesh CoC Attribution: - -Most of this is from the Recurse Center CoC. Other parts are from Strange Loop(community goals), Contributor Covenant (portion of the community goals, encouraged behaviors, enforcement guidelines), and the Toronto Mesh/Geek Feminism (guidelines for moderators). diff --git a/docs/community/community-networking-toolkit.md b/docs/community/community-networking-toolkit.md deleted file mode 100644 index 4b3b96f..0000000 --- a/docs/community/community-networking-toolkit.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Community Networking Toolkit ---- - -# Community Networking Toolkit - -This is a toolkit/resource guide for anyone interested in starting up their own community network. - -## Recommended Deployment Kit -Here is a [spreadsheet](https://docs.google.com/spreadsheets/d/1-f0neQ0XBOndDnIj8ft-SQVXU3Dub-5iQwYS_odf-MQ/edit#gid=0) containing a bunch of tools and equipment that you should bring to network deployments. This was created by Esther Jang who has a lot of practical experience deploying sites. This spreadsheet is color-coded into different categories based on how important they are to have! - -## Ethernet Crimping -Ethernet crimping is an important skill for network site installations. The length of an ethernet cable connection is only truly known once you're on site so it is useful to be able to quickly cut ethernet cable to the desired length and crimp it. - -* [Our Guide to Crimping](https://docs.seattlecommunitynetwork.org/learn/cable-crimping.html) -* [Workshop Slides](https://docs.google.com/presentation/d/1HG5OcJysTicr_JHOlsKTB2ewWBVexrQDYv0Xn3_1hYA/edit?usp=sharing) -* [Crimping Video](https://www.youtube.com/watch?v=WvP0D0jiyLg) -* [Cable Testing Video (first 7 min only for basics)](https://www.youtube.com/watch?v=3tHvOLBp2zM) -* [Ethernet Color Coding Diagrams](https://incentre.net/ethernet-cable-color-coding-diagram/) - * Both the T-568A and the T-568B standard Straight-Through cables are used most often as patch cords for your Ethernet connections. - * If you require a cable to connect two Ethernet devices directly together without a hub or when you connect two hubs together, you will need to use a Crossover cable instead. - -## Internet Society (ISOC) Materials -* [Introduction to Community Networks](https://www.internetsociety.org/issues/community-networks/) - -## All-aspects Community Networking Guides -* [AlterMundi (Spanish Language)](http://docs.altermundi.net/) -* [NYC Mesh - How to start a community network](https://www.nycmesh.net/blog/how/) -* [NYC Mesh Docs](https://docs.nycmesh.net/) -* [Telecommunications Reclaimed - Linked from NYC Mesh guide](https://www.netcommons.eu/sites/default/files/telecom-reclaimed-web-single-page.pdf) -* [Start your own ISP](https://startyourownisp.com/) - * This site is dedicated to helping you start your own Internet Service Provider. Specifically this guide is about building a Wireless ISP (WISP). -* [Commotion Construction Kit](https://commotionwireless.net/docs/cck/) - * The Commotion Construction Kit is a set of documentation tools that the Open Technology Institute has used in workshops around the world and at home. It is a “do it ourselves” guide to building community wireless networks. -* [Neighborhood Network Construction Kit](https://communitytechnology.github.io/docs/cck/) - * The Neighborhood Network Construction Kit is a set of documentation tools that the Open Technology Institute has used in workshops around the world and at home. It is a “do it ourselves” guide to building community wireless networks. Many of these activities were first released as part of the Commotion project. -* [Community Technology Field Guide](http://communitytechnology.github.io/) - * A collective resource for digital stewardship, digital justice and community infrastructure. These resources emphasize self-governance, participatory learning, collaborative design and sustainability. -* [Building Broadband Commons - Tools for Planners and Communities](https://static.newamerica.org/attachments/2445-building-broadband-commons/BuildingBroadband_v5_sm.0ce41b72f9cb49b9a0163c4aa8dfefa8.pdf) -* [Next Century City - Becoming Broadband Ready, a Toolkit for Communities](https://nextcenturycities.org/becoming-broadband-ready/) -* [Wireless Networking in the Developing World - Online Book](http://wndw.net/book.html#readBook) - * Wireless Networking in the Developing World is a free book about designing, implementing, and maintaining low-cost wireless networks. -* [Community Networks in Comics](https://ibebrasil.org.br/wp-content/uploads/2019/12/comic-en-v2.pdf) - * It is not easy to explain the concepts behind community networks, both the technical characteristics of radio frequency networks and the social and human aspects of community technologies. - * Teaching a workshop for popular groups using colonizing terms and methodologies can increase the existing barrier between people and a technology that was not created for their interests. - * With this in mind, images and analogies are powerful tools to make it easier to explain a technical term or an idea. We reject the premise that to do so would in any way underestimate people’s ability to understand technical matters. -* [Building Consentful Tech Zine](https://bklynlibrary.github.io/bklynConnect-curriculum/modules/prototyping%20consent/start/) - * In 2017, Una Lee, Dann Toliver, and their design firm And Also Too published the Building Consentful Tech Zine as a provocation on how to think about and design for consentfulness. - * This framing really resonated with our group, so we expanded it into a project where we prototype what this looks like in practice (and learned some interaction design methodologies on the way)! -* [Report on Digital Skill Sets for Diverse Users](https://www.seattle.gov/Documents/Departments/Tech/DigitalEquity/digital%20skills%20for%20diverse%20users.pdf) - * The City of Seattle, in partnership with Technology and Social Change Group (TASCHA), developed a set of Digital Equity Indicators that helps measure Seattle’s progress in meeting the initiative's goals. - * What digital skills do existing frameworks and curricula cover? - * What digital skills should the City and partners recommend to digital skill instructors to teach and promote? - * Do these resources have corresponding assessments to help assess individuals’ digital skill abilities? -* [Worksheet for Digital Skills for Diverse Users](https://www.seattle.gov/Documents/Departments/Tech/DigitalEquity/digital%20skills%20list-worksheet_DEN.docx) - * A list of 74 skills identified by TASCHA (see above resource) to include in digital equity curriculums -* [Report on current state of Detroit Community Network](https://drive.google.com/file/d/1kiByZFQ2KKTOWzB91D0yH0_UcDkRYg-a/view) - * In this case study, we focus on Detroit and the predominantly Black and lower-income neighborhood of the North End as an example of innovative, community-scale projects that are locally generated. -* [New Community Networks by Douglas Schuler](https://publicsphereproject.org/ncn/) - * Online book about building and socially sustaining community networks, based on Doug Schuler's experiences with the Seattle Community Network project in the 90's (1996) diff --git a/docs/community/join.md b/docs/community/join.md deleted file mode 100644 index 0ecd139..0000000 --- a/docs/community/join.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Join Us ---- - -# Join Us -Seattle Community Network has a place for **everyone**. Whether you'd like to join -to get free Internet, get involved to help out your community, learn some skills -so that you can get a job, or all of the above! - -**PRO TIP** Do everything on this page! Our community is spread out in a lot of different places, so doing everything is the best way to make sure you don't miss out. - -## Join our Discord! -Discord is a messaging platform that we use to stay in touch -with each other, organize our different teams, and update everyone on last minute things. - -This is a MUST do if you don't want to get left out! - -### Follow these steps to join our Discord server: - -1. Click the [invite link](https://discord.gg/mneaSskFT3) to enter the Discord server. -2. Introduce yourself on the `#introductions` channel and friend request one of the moderators so they can chat with you (you can write `@moderators` in your message to get their attention). They will need to add a role for you as a "member" so you can stay on the server before you log out, otherwise you will have to be invited and join again. -3. Install Discord on your [computer](https://discord.com/download), [Android](https://play.google.com/store/apps/details?id=com.discord&hl=en_US&gl=US), or [iOS](https://apps.apple.com/us/app/discord-chat-talk-hangout/id985746746) device to always stay up-to-date. - -## Chat with someone! -The best way to get involved with the network is to find someone that can direct you! Most of our work happens in teams. Here's how to [join a team](https://docs.seattlecommunitynetwork.org/community/teams.html). - -## Subscribe to our Google calendar! -On our Google calendar we post regular occurring meetings and any impromptu events like social events, emergency repairs, site visits, etc. -This is one of the only places to find out about the meetings our various teams are having! - -Join using one of these options: - -- Use this [link](https://calendar.google.com/calendar/u/1?cid=Y19hMWhjOXVwMTBjOWs2YTNnNmYyb20zN2c2b0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t) that should prompt you to add the calendar to your Google account -- Use this [ICS file](https://calendar.google.com/calendar/ical/c_a1hc9up10c9k6a3g6f2om37g6o%40group.calendar.google.com/public/basic.ics) to manually add the calendar via the iCalendar format. -- View the calendar [here](https://calendar.google.com/calendar/u/0/embed?src=c_a1hc9up10c9k6a3g6f2om37g6o@group.calendar.google.com&ctz=America/Los_Angeles) - -## Follow our social media! -Our social media team makes posts about upcoming meetings, social events, and they also occasionally -make educational posts! - -Don't miss out and follow us on [Instagram](https://instagram.com/seattlecommnet), [Facebook](https://facebook.com/seattlecommnet), and [Twitter](https://twitter.com/seattlecommnet). - -## Visit our Space! - diff --git a/docs/community/mission-vision-values.md b/docs/community/mission-vision-values.md deleted file mode 100644 index a04f6bd..0000000 --- a/docs/community/mission-vision-values.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Mission, Vision, Values ---- - -# Mission, Vision, and Values - -## Mission -LCL seeks to democratize knowledge, skills, and resources to enable people to establish and run their own local, community-centered, and community-owned Internet access networks and digital infrastructure. - -## Vision -We envision a world where no one is excluded from access to the Internet, and where anyone can achieve the expertise and capability to bring communications infrastructure to their community and improve their quality of life. - -## Values -We value the ability to access the Internet and all public information and digital resources therein as a human right. -- Digital privacy of our users and partner organizations -- Collaboration, especially with the communities and organizations we work with -- Care, consideration, allyship, and peer mentorship between individuals within our organization -- Education, sharing, and capacity-building- emphasize teaching and dissemination of information and skills -- Openness, transparency, and accountability of our organization and its processes -- Democratization and inclusiveness of decision processes among stakeholders -- Long-term sustainability of our technology deployments and community structures -- Equity in planning for resource allocation, programming, and contribution - diff --git a/docs/community/partners.md b/docs/community/partners.md deleted file mode 100644 index c9bc415..0000000 --- a/docs/community/partners.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Our Partners ---- - -# Our Partners - -- Althea -- API Chaya (WiFi is a Lifeline) -- Black Brilliance Research Project -- Breakfast Group -- Cham Refugees Community -- City of Seattle IT -- Filipino Community of Seattle -- King County Library System -- Oromo Cultural Center -- Seattle Public Schools -- Tacoma Cooperative Network -- Tacoma Public Library -- University of Washington diff --git a/docs/community/teams.md b/docs/community/teams.md deleted file mode 100644 index a5035ff..0000000 --- a/docs/community/teams.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: Join a Team ---- - -# Join a Team - -As a volunteer-run community network, we are always in need of extra hands. -If you're interested in helping out, check out what our teams are working on here! - -Feel free to have informational meetings with the respective team leads to learn more about how you can help. There's no commitment required and we welcome you to take on as much work as you have the capacity for. - -## Outreach -Our outreach team is responsible for finding new site host partners, finding users, and maintaining communications with our current partners. Message the [#outreach](https://discord.gg/mneaSskFT3) channel in Discord and contact Esther Jang to learn more about how you can get involved. - -This is the perfect team for you if any of the following apply to you: - -- Experience with community organizing -- Have community connections in the Greater Seattle Area -- Have cultural humility and experience partnering with marginalized communities -- Have the ability to translate and/or interpret into non-English languages common in the Seattle area such as Spanish, Vietnamese, Somali, Oromo, Khmer, and more. - -**As of July 2021, the primary objective** of the outreach team is to get connected with users for our network sites that fit any of the following criteria: - -- Unemployed -- Seniors -- Housing-unstable or houseless -- Non-English speaking - -## Social Media -Our social media team is in charge of our various accounts on [Instagram](https://www.instagram.com/seattlecommnet/), [Facebook](https://facebook.com/seattlecommnet), and [Twitter](https://twitter.com/seattlecommnet). They also develop branding and marketing materials for our various projects. Message the [#social-media](https://discord.gg/mneaSskFT3) channel in Discord and contact Firn Tieanklin to learn more about how you can get involved. - -You should join this team if you have experience in or are interested in practicing any of the following skills/technologies: -- Canva -- Design -- Marketing -- Photography/Videography - -## Web Development -Our web development team is working on redesigning and developing our new website. Message the [#website](https://discord.gg/mneaSskFT3) channel in Discord to learn more about how you can get involved. - -You should join this team if you have experience in or are interested in learning any of the following skills/technologies: - -- HTML/CSS -- Javascript -- Bootstrap -- React -- Redux -- Web Design (Using Figma) - -## Mobile App Development -Our mobile app development team is currently developing an Android app to record cell network performance metrics during our field tests. - -Message the [#measurement](https://discord.gg/mneaSskFT3) channel in Discord and contact Zhennan Zhou to learn more about how you can get involved. - -You should join this team if you have experience in or are interested in learning any of the following skills/technologies: - -- Android app development -- iOS app development -- Java -- Object oriented programming -- Open source development, Git, and GitHub - -## Education -Our education team plays a core role in our community networks as they enable our networks to be community-owned and operated. The education team is responsible for developing educational materials, running workshops, and teaching our Digital Steward cohorts. Message the [#digital-stewards](https://discord.gg/mneaSskFT3) channel in Discord and contact Esther Jang to learn more about how you can get involved. - -You should join this team if you have experience in or are interested in learning any of the following skills/technologies: - -- Teaching -- Curriculum development -- Computer networks, LTE networks, and community networks -- Linux -- Community organizing - -## Fundraising -Our fundraising team is currently setting up a crowdfunding platform to raise money for various expenses that this project requires. Our fundraising team is also working on applying for various community grants and research grants. Message the [#funding](https://discord.gg/mneaSskFT3) channel in Discord and contact Esther Jang to learn more about how you can get involved. - -## Accounting & Legal -Local Connectivity Lab, the nonprofit organizing that is incubating this project, often runs into accounting and legal challenges. If you are willing to provide pro bono services to benefit the community network, please contact Esther Jang at lcl@seattlecommunitynetwork.org. diff --git a/docs/community/tech-help.md b/docs/community/tech-help.md deleted file mode 100644 index cddbd74..0000000 --- a/docs/community/tech-help.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Community Tech Help ---- - -# Community Tech Help - -There are several ways to get community-based tech support from the SCN community, such as the [Help Desk](https://calendar.google.com/calendar/u/0/embed?src=c_grfefg3uklclsha3h8mhci8sdc@group.calendar.google.com&ctz=America/Los_Angeles). -You can use any of these methods to request Internet service from us, request general technology or computer help, or contact us about any other topic. - -## Join our Discord! - -The FASTEST way to get support will be to join the `#support` channel on our [Discord](https://discord.gg/mneaSskFT3), a messaging platform that we use to organize. - -### Follow these steps to join Discord: - -1. Join our [Discord server](https://discord.gg/mneaSskFT3) via the invite link. You will need to [log in](https://discord.com/login) or create an account to join, and a moderator will need to assign you a role before you are allowed to join permanently. -2. Install Discord for your [computer](https://discord.com/download), [Android](https://play.google.com/store/apps/details?id=com.discord&hl=en_US&gl=US), or [iOS](https://apps.apple.com/us/app/discord-chat-talk-hangout/id985746746) device to stay up-to-date on conversations. -3. When posting to the #support channel, describe your question or problem in as much detail as you can. Someone from the community will most likely respond within a few hours. -4. If you would like to get to know the community, introduce yourself in the `#introductions` channel! How did you hear about SCN and why are you interested? -(More complete instructions can be found [here](https://docs.seattlecommunitynetwork.org/get-started)). - -## Community-Run Help Desk - -The Seattle Community Network organizes a community-run tech help desk supported by the Black Brilliance Research Project's (BBR's) Digital Stewards Program and the Filipino Community of Seattle (FCS). Our current in-person help desk hours are on *Fridays at 3-5 pm* starting on 2/18/2022, located in the *Filipino Community Village Integrated Learning Center (ILC)*. - -Filipino Community Village Integrated Learning Center address: -5727 37th Ave S, Seattle, WA 98118 - -Our help desk is best-effort and mainly volunteer-based, so our virtual hours availability may be highly variable. -Please check our available hours and sign up for an appointment slot using the calendar link below if you can (or contact us another way if that doesn't work), as it helps the volunteers plan for our time. -However, you may also drop in during our scheduled calendar hours without an appointment. - -* [Our Help Desk Calendar](https://calendar.google.com/calendar/u/0/embed?src=c_grfefg3uklclsha3h8mhci8sdc@group.calendar.google.com&ctz=America/Los_Angeles): You can sign up for an appointment slot for any staffed volunteer hours indicated on the calendar. -* Phone Number (Voicemail-only except during staffed Community Tech Help hours): `(253) 655-7221` - * You may also send text messages to this number, which will be checked during staffed hours. - * Inquiries about getting connected to our Internet service can also be sent here. -* Email address for general Tech Support: [support@seattlecommunitynetwork.org](mailto:support@seattlecommunitynetwork.org) -* Email address for SCN Internet service-related support: [help@seattlecommunitynetwork.org](mailto:help@seattlecommunitynetwork.org) - -### Help Desk Volunteers - -We are actively recruiting more volunteers to help us run the help desk virtually, whenever and wherever that you are available. -Join our team by simply signing up on this [interest form](https://forms.gle/iqBYKmjkTYa4gFK98), we need you! - -Please let us know if you have additional questions or concerns in the `#support` channel on our [Discord](https://discord.gg/mneaSskFT3). - -## Documentation - -As always, please do not hesitate to consult our [docs](https://docs.seattlecommunitynetwork.org) for any information, or submit an issue on the docs site [github](https://github.com/Local-Connectivity-Lab/scn-documentation) if there is information missing that you would like to see. -Also feel free to message the Discord for the same purpose. diff --git a/docs/contribute/contribute.md b/docs/contribute/contribute.md deleted file mode 100644 index 098ec37..0000000 --- a/docs/contribute/contribute.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Contribute to SCN Docs ---- - -# Contribute to SCN Docs - -## Looking to help? - -If you wanna share resources and help improve our docs, this page will get you started! -Our docs are designed so that anyone can contribute. If this page isn't enough, contact one of -us and we'll be able to help you! - -Our documentation uses MkDocs ReadTheDocs theme (links at the bottom of the page) - -## Editing process -To edit this documentation you should: - -1. Get your own copy of the [repo](https://github.com/Local-Connectivity-Lab/scn-documentation) -1. Modify the documentation in your own repo (for more information see section on Local Development) -1. Submit a pull request -1. Wait for someone to review and accept the request - -The rest of this page will explain all the details you need to know about the directory structure, markdown, and other quirks for editing this documentation. Make sure to read everything! - -## Markdown files -All our documentation is stored in 'Markdown' files so that they can be easily -modified and changed without heavy technical knowledge. - -### Markdown Editors - -- A nice and simple online editor is [StackEdit](https://stackedit.io/app#) which will let you type in markdown and see what it would look like in realtime in split-screen. - -- Another option is [HackMD](https://hackmd.io/) which has the same features as StackEdit -but it also allows you to connect to your own GitHub repo and pull/push. - - This is a nice option if you are uncomfortable with using Git from the command line. - -## Local Development - -### Using MkDocs -[MkDocs](https://www.mkdocs.org/) is pretty simple, just install it through pip then you can run `mkdocs serve` to locally view the website it will generate. - -## Documentation Directory Structure -Each directory has a `.site` file that declares the order each file/folder in that directory will show up. -Children pages are implicit in the directory structure - -## Static Files - -If you need static files for any of your pages, you should put them in the `assets` folder within the top level `docs` folder - -For example, for the cable-crimping page, the images for the tutorial are located in the "assets/cable-crimping" folder. These images can then be referenced relatively with the following standard markdown image syntax: -```markdown -![RJ45 Crimping Tool](../assets/cable-crimping/kit-crimping-tool.jpg) -``` diff --git a/docs/faq/.pages b/docs/faq/.pages deleted file mode 100644 index f1d4443..0000000 --- a/docs/faq/.pages +++ /dev/null @@ -1,10 +0,0 @@ -nav: - - connection.md - - what.md - - why.md - - about.md - - how.md - - site.md - - help.md - - diff --git a/docs/faq/about.md b/docs/faq/about.md deleted file mode 100644 index 665fb3e..0000000 --- a/docs/faq/about.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: What is the Seattle Community Network? ---- - -# About Seattle Community Network - -Seattle Community Network (SCN) is a community network dedicated to providing fair access to underserved communities all across the Puget Sound. SCN is a project of Local Connectivity Lab, a 501(c)(3) registered non-profit that works to share free or low-cost broadband access in higher-need areas throughout the Puget Sound region, making use of existing network infrastructure such as buildings and fiber-optic cables to extend coverage to more people. - -As a community network, we rely on the help of local residents such as yourself to maintain and grow the network. [Joining us](../community/join.md) is a great way to become an active member of your own community, make friends, and learn valuable technical skills. diff --git a/docs/faq/connection.md b/docs/faq/connection.md deleted file mode 100644 index d93b7b5..0000000 --- a/docs/faq/connection.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: How do I get Internet? ---- - -# How do I get Internet from the Seattle Community Network? - -## Eligibility -The Seattle Community Network exists to provide free or low-cost internet to low-income and in-need users. - -We prioritize serving the following groups: - -- low-income families of students -- unemployed adults (looking for work) -- majority non-English speaking adults/families -- seniors - -## Registration -To connect to the internet through the Seattle Community Network, you will need to register with us. - -To register, you can: -- Email lcl@seattlecommunitynetwork.org -- Contact us by phone at (253) 655-7221 and leaving a voice mail or text. - -## Hardware -Once your registration is processed, you will receive the necessary hardware to connect to the network. diff --git a/docs/faq/help.md b/docs/faq/help.md deleted file mode 100644 index 9dc2e22..0000000 --- a/docs/faq/help.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: How can I Help? ---- - -# How can I Help? - -## Volunteer! - -SCN is run completely by volunteers. There are many ways you can help, and no technology skills are required. We need help with everything from setting up network hardware and developing software to community outreach and fundraising. If you want to help, we can use your talents! - -First, make sure you [get connected with our community](../community/join.md). - -Next, why not [Join a Team](../community/teams.md) or [Contribute to SCN Docs](../contribute/contribute.md)? \ No newline at end of file diff --git a/docs/faq/how.md b/docs/faq/how.md deleted file mode 100644 index 16fefdf..0000000 --- a/docs/faq/how.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: How does SCN Work? ---- - -# How Does the Seattle Community Network Work? - -The Seattle Community Network partners with the University of Washington to share free or low-cost internet access with areas of higher need. - -The Seattle Community Network (SCN) is a wireless Internet access network using 4G LTE and WiFi technologies, providing public access from partner locations such as libraries, schools, businesses, and community centers. The Internet connection at these sites is shared wirelessly to nearby devices using the 4G LTE (cell-phone) data standard, which can be used by certain phones and hotspots (also known as Customer Premises Equipment or CPE). Individual users can connect to this signal using SCN-provided (or other compatible) devices to create a local WiFi network in their home. Some of our installed sites utilize a portion of the University of Washington's internet bandwidth, sharing it out to further neighborhoods via point-to-point wireless links. Some sites use other upstream internet service providers (ISPs) such as Lumen. - -The network is completely created, managed, and maintained by volunteers with a range of diverse skills in information technology and beyond. All infrastructure is paid for by generous donations from sponsors and the public. Speaking of which, why not [volunteer](../community/join.md) or [donate](https://seattlecommunitynetwork.org/donate.html)? \ No newline at end of file diff --git a/docs/faq/site.md b/docs/faq/site.md deleted file mode 100644 index fc8c695..0000000 --- a/docs/faq/site.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: What is this site? ---- - -# About This Website -The Seattle Community Network Docs website is the central hub for information about our community and networks. Here, we describe our infrastructure, how to set-up hardware and software, how you can start your own community network, our community rules, and more. - -This website is maintained by our volunteers, much like the rest of our services. This means you can help us improve it by adding missing information, clarifying confusing points, or even just fixing typos you notice while you’re reading. See [Contribute to SCN Docs](../contribute/contribute.md) to learn more about how you can contribute to this website. - -If you are looking for our main website, it is located at [www.seattlecommunitynetwork.org](https://www.seattlecommunitynetwork.org). \ No newline at end of file diff --git a/docs/faq/what.md b/docs/faq/what.md deleted file mode 100644 index c2841c8..0000000 --- a/docs/faq/what.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: What is a Community Network? ---- - -# What is a Community Network? -"Community Networks (CNs) are crowd-sourced collaborative networks, developed in a bottom-up fashion by groups of individuals – i.e. communities – that design, develop and manage the network infrastructure as a common resource. Importantly, at the centre of CNs and the socio-economic ecosystems they generate lay the communities and their members, who are essential to initiate, maintain and guarantee the success of these connectivity efforts." - -Source: [Building Community Network Policies: A Collaborative Governance towards Enabling Frameworks](https://comconnectivity.org/building-community-networks-a-collaborative-governance-towards-enabling-frameworks/) \ No newline at end of file diff --git a/docs/faq/why.md b/docs/faq/why.md deleted file mode 100644 index 9872c30..0000000 --- a/docs/faq/why.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Why have a Community Network? ---- - -# Why Have a Community Network? - -Internet access is a foundational component of many aspects of modern life, nearly as important as electricity and water service. Though internet service is generally available in many areas of the United States, there are still areas that are underserved because of a variety of geographic and socio-economic factors, or simply because traditional internet service providers do not find it profitable to install and maintain the necessary infrastructure to serve some areas with adequate internet. - -Community networks attempt to address this digital divide by connecting underserved communities. Because a community network is created, managed, and maintained by volunteers, it is able to serve areas that may not have other affordable internet options. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 673ad65..0000000 --- a/docs/index.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Home ---- - -# Seattle Community Network Docs -Welcome to the documentation website for the [Seattle Community Network](https://seattlecommunitynetwork.org/)! If you're looking for our main website, it is located at [https://www.seattlecommunitynetwork.org](https://seattlecommunitynetwork.org/). - -## You're in the Right Place - -Seattle Community Network has a place for **everyone**. Whether you'd like to join -to [get free Internet](faq/connection.md), get involved to help out your community, learn some skills -so that you can get a job, or all of the above! - -**PRO TIP** We are a **community**. It's in our name! So, why not start by [joining our community](community/join.md)? It's easy. - -## What's here? -Some topics you can find on this website include: - -- FAQ - get the answers to some common questions. -- Community - get involved and learn more about our community, our rules, and what we're up to. -- Learn - gain some new skills that you can use to help out with our networks. -- Infrastructure - get the details on how our networks work behind the scenes. \ No newline at end of file diff --git a/docs/infrastructure/.pages b/docs/infrastructure/.pages deleted file mode 100644 index 37c8ff6..0000000 --- a/docs/infrastructure/.pages +++ /dev/null @@ -1,9 +0,0 @@ -nav: - - hardware.md - - software.md - - peering.md - - librenms-manager-setup.md - - librenms-setup.md - - epc-setup.md - - sas-setup.md - - proxmox-vaultwarden-deployment.md \ No newline at end of file diff --git a/docs/infrastructure/epc-setup.md b/docs/infrastructure/epc-setup.md deleted file mode 100644 index 292c4e2..0000000 --- a/docs/infrastructure/epc-setup.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Step 1. LTE Core Network Setup ---- - -# Step 1: CoLTE/EPC (LTE Core Network) Setup - -Our core networks use the [CoLTE project](https://github.com/uw-ictd/colte) maintained by the [UW ICTD Lab](https://ictd.cs.washington.edu/). - -For information on how to install and configure CoLTE, visit the [tutorial](https://docs.colte.network/tutorials/epc-setup.html) we wrote with them! diff --git a/docs/infrastructure/hardware.md b/docs/infrastructure/hardware.md deleted file mode 100644 index 3fa73ec..0000000 --- a/docs/infrastructure/hardware.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: Hardware Overview ---- - -# Our Hardware - -This page will be an overview of some of the core pieces of hardware that we use to deploy our sites. - -This page is in development, please contact us at lcl@seattlecommunitynetwork.org if you would like to learn more about the hardware we use. - -**TODO** -======= -## Network Site Equipment - -### Base Station (eNodeB) -![Baicells Nova 233 Base Station Marketing Image](https://www.doubleradius.com/images/Nova-233-3-5GHz-1W-Gen2-mBS1105_02.jpg?resizeid=3&resizeh=1000&resizew=1000) - -Baicells Nova 233 3.5GHz 1W Gen2 - -More info [here](https://www.doubleradius.com/baicells-nova-233-gen-2-enodeb-outdoor-base-station) - -### Panel Antennas (eNodeB) -![Alpha Wireless Antenna Marketing Image](https://www.lastmilegear.com/wp-content/uploads/2017/12/aw3014.jpg) - -Alpha Wireless, 3.3-3.8GHz, 2x2 MIMO, 18dBi, +/-45°, 65° - -More info [here](https://www.lastmilegear.com/shop/alpha-aw3014/) - -### Core Network Computer (EPC) -![Qotom Mini PC Marketing Image](https://www.qotom.net/upload/thumb_src/400_400/1526031726.jpg) - -Qotom Mini PC Q190G4N S07 - -Key features: -- 4 ethernet ports -- designed to be run 24/7 -- small and quiet -- cheap - -More info [here](https://www.qotom.net/product/36.html) - -## User Access Devices - -### LTE Consumer Premises Equipment (CPE) -![Baicells Atom CPE Marketing Image](https://www.lastmilegear.com/wp-content/uploads/2018/02/Baicells-Atom-eg8035L.jpg) - -Baicells Atom OD04 3.5GHz 14dBi - -More info [here](https://www.lastmilegear.com/shop/atom-od04-3-5ghz-14dbi-gen2/) - -### Outdoor WiFi Router -![Mikrotik OmniTIK 5 PoE ac Marketing Image](https://www.wifi-stock.com/full/omnitik_5ac.jpg?size=10) - -Mikrotik OmniTIK 5 PoE ac - -Outdoor router of choice for NYC Mesh, so it has been tried and tested. Good balance of quality and price. - -More info [here](https://mikrotik.com/product/rbomnitikpg_5hacd) - -### Home WiFi Router -![TP-Link Archer A5 Router Marketing Image](https://m.media-amazon.com/images/I/51R2a9p-vNL._AC_SS450_.jpg) - -TP-Link Archer A5 Router - -More info [here](https://www.tp-link.com/us/home-networking/wifi-router/archer-a5/) - -### CBRS-Compatible Unlocked Smartphone - -We purchase refurbished Google Pixel 4 smartphones because they are affordable, provide all -necessary smartphone features, and are CBRS-compatible. - -Note that purchasing CBRS-compatible phones can be a logistical challenge. We've experienced trouble purchasing -from vendors that send incorrect models of phones that don't support CBRS band and we had to go back and forth. -Test your phones before distributing them! - -Here is [one spot](https://www.backmarket.com/search?q=pixel%204&ga_search=pixel%204) to purchase refurbished phones. diff --git a/docs/infrastructure/librenms-manager-setup.md b/docs/infrastructure/librenms-manager-setup.md deleted file mode 100644 index c59f013..0000000 --- a/docs/infrastructure/librenms-manager-setup.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -title: Network Monitoring 1. LibreNMS Network Manager Configuration ---- - -# LibreNMS Network Manager Configuration - -Seattle Community Networks uses SNMP to monitor network nodes. LibreNMS is used for Network Management, Dashboard generation and Alerting. - -## LibreNMS Manager Installation: -[Install LibreNMS](https://docs.librenms.org/Installation/Install-LibreNMS/) -[Install and Configure LibreNMS on Ubuntu with nginx](https://computingforgeeks.com/how-to-install-and-configure-librenms-on-ubuntu-with-nginx/) - -## Network-Specific Configuration: -Change active user to librenms: -```sudo su - librenms``` - -Edit /opt/librenms/config.php: - -```php -'); -$config['auth_mechanism'] = "mysql"; # default, other options: ldap, http-auth -$config['nets'][] = "10.0.0.0/24"; # Replace with your Management Network Subdomain -$config['rrd_purge'] = 0; -$config['enable_billing'] = 1; -$config['show_services'] = 1; -``` - -As user 'librenms', run /opt/librenms/snmp-scan.php, to scan the configured network for snmp hosts - -## Adding Baicells OS configuration to LibreNMS - -As user 'librenms' on the librenms server, create the following files and update their contents accordingly: -* For OS detection, ~librenms/includes/definitions/rts.yaml: -``` - os: rts - text: 'Baicells RTS' - type: network - icon: rts - over: - - { graph: device_bits, text: 'Device Traffic' } - - { graph: device_processor, text: 'CPU Usage' } - - { graph: device_mempool, text: 'Memory Usage' } - discovery: - - sysDescr: - - 'CELL' -``` - -* For defining custom RTS OS sensors, ~librenms/includes/definitions/discovery/rts.yaml: - -``` -mib: BAICELLS-MIB -modules: - os: - hardware: BAICELLS-MIB::hardwareVersion.0 - serial: BAICELLS-MIB::sn.0 - version: BAICELLS-MIB::softwareVersion.0 - sensors: - count: - data: - - - oid: ulThroughput - num_oid: '.1.3.6.1.4.1.53058.190.7.{{ $index }}' - descr: 'Upload Throughput' - group: 'Throughput' - index: 'ulthroughput.{{ $index }}' - - - oid: dlThroughput - num_oid: '.1.3.6.1.4.1.53058.190.8.{{ $index }}' - descr: 'Download Throughput' - group: 'Throughput' - index: 'dlThroughput.{{ $index }}' - - - oid: ulPrbUtilization - num_oid: '.1.3.6.1.4.1.53058.190.9.{{ $index }}' - descr: 'Upload PRB Utilization' - group: 'Utilization' - index: 'ulPrbUtilization{{ $index }}' - - - oid: dlPrbUtilization - num_oid: '.1.3.6.1.4.1.53058.190.10.{{ $index }}' - descr: 'Download PRB Utilization' - group: 'Utilization' - index: 'dlPrbUtilization.{{ $index }}' - frequency: - data: - - - oid: carrierBwMhz - num_oid: '.1.3.6.1.4.1.53058.100.7.{{ $index }}' - divisor: 5 - descr: 'Carrier Bandwidth' - index: 'carrierBwMhz.{{ $index }}' - percent: - data: - - - oid: eRABEstablishSuccessRate - num_oid: '.1.3.6.1.4.1.53058.190.3.{{ $index }}' - descr: 'ERAB Establishment Success Rate' - group: 'LTE' - index: 'eRABEstablishSuccessRate.{{ $index }}' - - - oid: hoSuccInterEnbS1Rate - num_oid: '.1.3.6.1.4.1.53058.190.4.{{ $index }}' - descr: 'Inter MME S1 Handover Success Rate' - group: 'LTE' - index: 'heSuccInterEnbS1Rate.{{ $index }}' - - - oid: hoSuccInterEnbRate - num_oid: '.1.3.6.1.4.1.53058.190.5.{{ $index }}' - descr: 'Inter MME Handover Success Rate' - group: 'LTE' - index: 'hoSuccInterEnbRate.{{ $index }}' - - - oid: rrcBuildSuccessRate - num_oid: '.1.3.6.1.4.1.53058.190.6.{{ $index }}' - descr: 'RRC Build Success Rate' - group: 'LTE' - index: 'rrcBuildSuccessRate.{{ $index }}' -``` - -* For defining a custom OS class to use Wireless sensors, ~librenms/LibreNMS/OS/Rts.php (note: pay attention to capitalization) - -```php -getDeviceId(), $oid, 'rts', 1, 'UE Connections') - ); - } -} -``` - -* A nice looking logo, ~librenms/html/images/os/rts.png -[Download an example Baicells Logo Here](https://imgur.com/9AOohPr.png) - -* Download the baicells mib from [this link](https://na.baicells.com/download/RTS%203.6%20BAICELLS-MIB.mib), and save it to ~librenms/mibs/BAICELLS-MIB (note: no file extension) diff --git a/docs/infrastructure/librenms-setup.md b/docs/infrastructure/librenms-setup.md deleted file mode 100644 index c3cac96..0000000 --- a/docs/infrastructure/librenms-setup.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Network Monitoring 2. LibreNMS Agent Configuration ---- - -# LibreNMS Agent Configuration - -## Adding a New Node to LibreNMS - -Both the eNodeB and the ePC must be configured individually in order for them to report statistics to the SNMP Manager. Since the eNodeB is not directly accessible from the management VPN, we configure an SNMP proxy on the ePC to pass SNMP statistics to the Management host. - -## ePC SNMP Configuration - -* Install snmpd to the ePC node: -``` $ sudo apt install snmpd ``` - -* Modify /etc/snmp/snmpd.conf: - -``` -sysLocation -sysContact lcl@seattlecommunitynetwork.org -sysServices 72 -master agentx -agentAddress udp:161 -com2sec readonly -com2sec -Cn ctx_baicells readonly enodeb -group readonlygroup v2c readonly -view all included .1 -access readonlygroup "" v2c noauth exact all none none -access readonlygroup ctx_baicells v2c noauth prefix all none none -proxy -Cn ctx_baicells -v 2c -c private 192.168.151.1 .1.3 -``` - -This configuration allows us to access SNMP data on the EPC with the standard community string (refer to internal standards documentation). but will proxy the Baicells SNMP data when we send the community string ‘enodeb’ - -* Update the snmpd service file to automatically restart snmpd on crash: - * Edit /lib/systemd/system/snmpd.service, modify the 'ExecStart' line, and add the 'ExecReload', 'Restart', and 'RestartSec' lines: - -``` -[Unit] -Description=Simple Network Management Protocol (SNMP) Daemon. -After=network.target -ConditionPathExists=/etc/snmp/snmpd.conf - -[Service] -Type=simple -ExecStartPre=/bin/mkdir -p /var/run/agentx -ExecStart=/usr/sbin/snmpd -LO2w -u Debian-snmp -g Debian-snmp -I -smux,mteTrigger,mteTriggerConf -f -p /run/snmpd.pid -ExecReload=/bin/kill -HUP $MAINPID -Restart=on-failure -RestartSec=5s - -[Install] -WantedBy=multi-user.target -``` - -* Enable and restart snmpd: -``` -Sudo systemctl daemon-reload -sudo systemctl enable snmpd -sudo systemctl restart snmpd -``` - -## Baicells SNMP configuration -* Log into the Baicells configuration console: -```https://``` - -* From the left menu, select System - -* Select SNMP -![Example Screenshot: enabling SNMP in the Baicells Console](https://i.imgur.com/YanPtMs.png) - * Under ‘SNMP Switch,’ select ‘Enable’ - * Configure the following options: - * Community String: private - * Contact: lcl@seattlecommunitynetwork.org - * Location: \ (String should not have any spaces) - * Source: Any - -## Adding the Node to LibreNMS -* If the ePC is running, librenms should be able to auto-discover it. Run this command from a shell on the management host: -```sudo -u librenms lnms scan``` - -* LibreNMS should print a status message that it was able to add a new device. - -* When first discovered, the ePC will show up generically as it’s ip address. Edit the hostname, but clicking ‘Edit Device’ (gear icon): - * Click the red pencil icon, and change the ip address to the hostname - * Fill ‘Overwrite IP’ with the ePC IP address - * *Note: If the IP is not changed to the hostname, you will not be able to add the eNodeB by it’s IP address* -![Example Screenshot: Updating ePC Hostname in LibreNMS](https://i.imgur.com/LHeL3Zq.png) - -* The Baicells eNB needs to be added manually: From LibreNMS, select Devices and click “Add Device” -![Example Screenshot: Manually adding eNodeB to LibreNMS](https://i.imgur.com/Tlqpbh3.png) - -* Add a new device, with the following configurations: - * Hostname: - * Community: ‘enodeb’ - * Force Add: On - -* *Note: If you receive an error message stating that a device with the specified IP already exists, make sure that you have* successfully changed the eNodeB’s hostname per the previous step. - -* Once the device is added, click the ‘Edit Device’ icon (gear icon) and update the following values: - * Display name: \ - * Overwrite device contact: lcl@seattlecommunitynetwork.org - -## Other helpful notes: - -* [Baicells eNB config guide](https://img.baicells.com//Upload/20210810/FILE/195c7e84-47d9-4acb-aa00-cba0e080d885.pdf) - -* How to SSH into Baicells eNB: - * SSH using port 27149 (username same as normal web-based login) - * Convert the MAC address of this eNB to link local address: http://www.sput.nl/internet/ipv6/ll-mac.html - diff --git a/docs/infrastructure/peering.md b/docs/infrastructure/peering.md deleted file mode 100644 index 02d7318..0000000 --- a/docs/infrastructure/peering.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Public ASN Peering ---- -# Public ASN Peering -### Local Connectivity Lab operates **AS54429** - -## Our peering Policy is **Yes** - -Please [contact us](mailto:lcl@seattlecommunitynetwork.org) to peer with our network. - -Note this network is our public ASN, not the [Seattle Community Network](https://seattlecommunitynetwork.org) itself. If you would like to join the network visit our [connect](https://seattlecommunitynetwork.org/ourSites.html) page. - -Seattle Community Network (SCN) is a community network dedicated to providing fair access to underserved communities all across the Puget Sound. [Learn more on our FAQ](../faq/about.md). - -### Peering Policy - -* Local Connectivity Lab has an open peering policy. -* We have no requirements in terms of traffic, size, support/SLA, etc. -* We operate both IPv4 and IPv6. Peering via both protocols is appreciated. - -### Locations - -| Building | Address | Ports | -| -------- | ---------------------------- | -------- | -| Westin | 2001 6th Avenue, Seattle, WA | 1G / 10G | - -### Exchanges - -| Exchange | City | IPv4 | IPv6 | ASNs | Routes | Speed | -| ------------------------------- | ------------ | -------------- | ---------------- | ---- | ------ | ----- | -| Seattle Internet Exchange (SIX) | Seattle, WA | 206.81.81.150 | 2001:504:16::d49d | 336 | ~192K | 10G | - -## Peering Data - -ASN: 54429 -Peering Contact: tech@seattlecommunitynetwork.org -PeerDB Page: [https://as54429.peeringdb.com](https://as54429.peeringdb.com) -As we are a non-profit, please consider providing as many routes as possible, including upstream or other routes. diff --git a/docs/infrastructure/proxmox-vaultwarden-deployment.md b/docs/infrastructure/proxmox-vaultwarden-deployment.md deleted file mode 100644 index f9e19fc..0000000 --- a/docs/infrastructure/proxmox-vaultwarden-deployment.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -title: Proxmox Deployment Guide - Vaultwarden ---- - -### Deployed by: Esther Jang, Paul Phillion, Rudra Singh - ---- - -## Section 1: What is Proxmox? - -- **Proxmox VE (Virtual Environment)** is an open-source server management platform designed to deploy and manage virtual machines (VMs) and containers. -- It integrates KVM hypervisor and LXC containers, enabling users to manage virtual infrastructure through a web-based interface. - -## Section 2: Access Requirements for Proxmox VE at Seattle Community Network - -- To submit a request for a SCN self-hosted Proxmox VM on our private cloud, please fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSf6NYZHTfxi_hcQM1DWJ8mhgJDl-iiqRL4yTQG_x1az6XEfEQ/viewform?usp=sf_link). -- If you are working on a project for SCN and need access to the Proxmox VE, please continue reading. -- Next, you will need access to the OpenVPN. Proxmox VE can only be accessed on the VPN. Specific details can be obtained from the SCN discord. Once connected to the VPN, a specific IP will be provided for you to access the Proxmox VE, where you can input your credentials. - -## Section 3: Setting up your VM - -- ### Install SSH -- ### Install Keys -- ### Test SSH CLI - -## Section 4: SSH Troubleshooting - -- Please let the SCN discord know if you have trouble SSH'ing in. A useful command during setup was `sudo ufw status`. If it is active, use `ufw disable`. The expected status should be inactive. If that still doesn’t work, try restarting the VM from the Proxmox VE. - -## Section 5: Beginning Deployment: Vaultwarden - -```bash -docker pull vaultwarden/server:latest -docker run -d --name vaultwarden -v /vw-data/:/data/ --restart unless-stopped -p 80:80 vaultwarden/server:latest -``` - -## Section 6: Docker Compose - -- Create a Docker Compose file: - -```yaml -version: "3" - -services: - vaultwarden: - container_name: vaultwarden - hostname: vaultwarden9 - ports: - - "127.0.0.1:8080:80" - environment: - - LOG_FILE=/log/access.log - - LOG_LEVEL=info - - EXTENDED_LOGGING=true - image: vaultwarden/server:latest - restart: unless-stopped - volumes: - - /opt/vw-data:/data - - /var/log/vw:/log -``` - -- To start and run your container application in detached mode, use: - -```bash -docker-compose up -d -docker-compose start -docker-compose stop -docker-compose restart -``` - -## Section 7: Azure DNS - -- For this deployment, we're utilizing a public IP address. This address must be configured within Azure DNS to point to our chosen domain name. -- Navigate to Home - Microsoft Azure with your account. -- Open “DNS zones”. If you were using a private IP, you would use “Private DNS zones”. -- We will be creating a subdomain under seattlecommunitynetwork.org. -- Click on that, and under DNS management, click recordsets, and click add. -- Insert the beginning of the subdomain name, which in our case is “vaultwarden”. -- Below, under IP address, insert your IP, and create your new subdomain. - -## Section 8: Enabling Nginx - -```bash -sudo apt update -sudo apt install nginx -sudo systemctl enable nginx -sudo systemctl start nginx -``` - -- Next, go to `/etc/nginx/sites-enabled/default`. Delete everything inside `default` and paste this: - -```nginx -# The `upstream` directives ensure that you have a http/1.1 connection -# This enables the keepalive option and better performance -# -# Define the server IP and ports here. -upstream vaultwarden-default { - zone vaultwarden-default 64k; - server 127.0.0.1:8080; - keepalive 2; -} - -# Needed to support websocket connections -# See: https://nginx.org/en/docs/http/websocket.html -# Instead of "close" as stated in the above link we send an empty value. -# Else all keepalive connections will not work. -map $http_upgrade $connection_upgrade { - default upgrade; - '' ""; -} - -# Redirect HTTP to HTTPS -server { - listen 80; - listen [::]:80; - server_name vaultwarden.seattlecommunitynetwork.org; - - if ($host = vaultwarden.seattlecommunitynetwork.org) { - return 301 https://$host$request_uri; - } - return 404; -} - -server { - # For older versions of nginx appened http2 to the listen line after ssl and remove `http2 on` - listen 443 ssl; - listen [::]:443 ssl; - # http2 on; - server_name vaultwarden.seattlecommunitynetwork.org; - - # Specify SSL Config when needed - ssl_certificate /etc/path...; - ssl_certificate_key /etc/path...; - ssl_trusted_certificate /etc/path...; - - client_max_body_size 525M; - - location / { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_pass http://vaultwarden-default; - } - - # Optionally add extra authentication besides the ADMIN_TOKEN - # Remove the comments below `#` and create the htpasswd_file to have it active - # - #location /admin { - # See: https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/ - #auth_basic "Administrator's Area"; - #3auth_basic_user_file /path/to/htpasswd_file; - - #proxy_http_version 1.1; - #proxy_set_header Upgrade $http_upgrade; - #proxy_set_header Connection $connection_upgrade; - - #proxy_set_header Host $host; - #proxy_set_header X-Real-IP $remote_addr; - #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - #proxy_set_header X-Forwarded-Proto $scheme; - #proxy_pass http://vaultwarden-default; - } -} -``` - -## Section 9: Obtain SSL Certificates - -```bash -sudo apt install certbot python3-certbot-nginx -sudo certbot --nginx -d vaultwarden.seattlecommunitynetwork.org -``` - -- Certbot will modify your Nginx configuration to handle HTTPS and redirect from HTTP to HTTPS. -- Ensure that you copy the file paths provided at the conclusion of the certbot process into the default nginx configuration file, replacing the corresponding comments with these paths. -## Section 10: Ensure Everything is Running - -```bash -sudo systemctl status nginx -``` - -## Section 11: Additional Features - -- Enabling admin panel: - -Go back to `/etc/nginx/sites-enabled/default` and uncomment the admin section at the bottom. Follow directions at [Nginx Admin Guide](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/) to encrypt your admin password as a `.env` file (preferably using argon CLI). - -Once done, make sure you create a `.env` file in the directory where the compose file is with `VAULTWARDEN_ADMIN_TOKEN=[insert your hashed admin token]`. - -Then in your compose, add these two lines under environment: - -```yaml -- ADMIN_TOKEN=${VAULTWARDEN_ADMIN_TOKEN} -- DOMAIN=https://vaultwarden.seattlecommunitynetwork.org -``` - -Restart the container and try logging into . - -Once logged in, SMTP and 2FA enabling settings can be configured on the home page. - -## Section 12: Data Backup - -- Enabling backups for Vaultwarden is a simple process. Please review the attached backup bash script, which facilitates the transfer of Vaultwarden data to another virtual machine. Additionally, there is a cleanup bash script designed to retain only the most recent file in the other VM, deleting all others. Feel free to modify the scripts as necessary to suit your specific requirements. - - - - - - -Backup script: -```bash -#!/bin/bash - -docker-compose down -datestamp=$(date +%m-%d-%Y) -zip -9 -r /home/scn/backups/${datestamp}.zip /opt/vw-data* -scp -i ~/.ssh-comm/id_rsa /home/scn/backups/${datestamp}.zip azureuser@[IP address]:~/backups/ -docker-compose up -d -``` - -Cleanup Script: -```bash -#!/bin/bash - -# Define the directory containing backup files -backup_dir=~/backups - -# Go to the backup directory -cd "$backup_dir" || exit - -# Find and delete older backup files (excluding the latest day) -find . -type f -name '*.zip' ! -mtime -1 -exec rm {} + - -# Exit -exit 0 -``` -## Section 13: Using the Backup -To restore a data backup to the original virtual machine, simply unzip the file and delete the existing contents of `/opt/vw-data`. Then, transfer the contents of your zip file into this directory. Perform a quick restart of the container, and you will have successfully restored the version of the backup you selected. diff --git a/docs/infrastructure/sas-setup.md b/docs/infrastructure/sas-setup.md deleted file mode 100644 index 305e8b2..0000000 --- a/docs/infrastructure/sas-setup.md +++ /dev/null @@ -1,277 +0,0 @@ ---- -title: Step 2. eNodeB and SAS Setup ---- - -# Step 2: eNodeB and SAS Setup - -## Introduction -Despite CBRS being a relatively open frequency band, the processes for spectrum access are still somewhat opaque and require significant capital investment and/or ISP-level resources to set up. To clarify this process, here’s a step by step walkthrough tutorial of the setup of a Baicells eNodeB (eNB) base station running in the Citizen’s Broadband Radio Service (CBRS) spectrum band (or band 48). - -Before following this tutorial, you should have completed the setup of a LTE Evolved Packet Core (EPC) to control your eNB, for which the setup of an open source version based on open5gs is outlined in this [tutorial](https://hackmd.io/brHS3l1-T_uTUaDEAHMOxw?view). - -## I. Get set up with a Spectrum Access System (SAS) - -### A. Why get set up with a SAS? -Current FCC regulations require all CBRS equipment (called a CBSD) to be registered on a Spectrum Access System (SAS) that coordinates all spectrum assignments and ensures that no transmissions interfere with each other. This will likely require a commercial agreement with a SAS provider such as Google, Federated Wireless, etc. **This tutorial uses the Google SAS.** - -### B. CPI License -At least one member of your team will require “Certified Professional Installer” (CPI) training and license in order to hold legal responsibility for and sign off on device installations. Most SAS providers will offer training at about $500 for both an online training course and the certification exam. If you aren’t able to get someone on your team certified, be sure to collaborate with a CPI! Feel free to contact us at the Local Connectivity Lab if you need support for your community project in this regard, and we can figure out what is feasible. - -The following are some links and helpful notes about this process: -* https://wifidevan.wordpress.com/cbrs-certified-professional-installer-cpi-study-notes/ -* https://alliancecorporation.ca/webinars/webinars-webinars/cbrs-for-beginners-part-2-by-commscope/ -* https://cbrs.wirelessinnovation.org/acronyms - -### C. SAS Pricing Agreements - -For Google, the price options provided us in summer 2020 were: - -* Fixed Wireless - * SAS services are billed per link/household so you pay for each CPE (Customer Premises Equipment) CBSD registered with SAS. - * CBSDs that operate as base stations are free of charge. Price Per Customer Link $2.25/month. -* Mobility/Private LTE (price is based on CBSD categoris) - * Category A CBSD - * max transmit capability: 30 dBm/10 MHz = 20 dBm/MHz or “1 Watt” - * mounted under 6m Height Above Average Terrain measured 3-16 km away from site - * $2.67/month - * Category B CBSD - * max transmit capability: Maximum EIRP of 47 dBm/10 MHz = 37 dBm/MHz or “50 Watt" - * $13.33/month. - -### D. SAS Registration - -CBSDs must register their transmit capabilities with the SAS using either the “one-step” or “multi-step” process. - -The one-step process requires you to input all installation parameters and sign them with the CPI certificate all on the base station itself, or via a cloud domain proxy such as used by Baicells. **Not all base stations support this and the interfaces for doing so might vary widely, so “multi-step” is typically recommended.** - -## II. Register device in SAS portal -This tutorial will be walking through steps following the specifics of the Google SAS portal interface, but the steps should be generalizable to other SAS portals. - -### A. Once you have an account on an SAS service, register your devices on their portal or dashboard. -The Google SAS portal can be found at: https://wirelessconnectivity.google.com/sas/ - -### B. Our Setup - -Our test setup in the lab includes: - -- 1W Baicells Nova 233 base station in the CBRS band mounted on the 6th floor balcony of our UW computer science building. -- Alpha Wireless 18 dBi-gain panel antenna with a beamwidth of 65 degrees (model AW3014-T4), mounted straight ahead and not tilted down. - - -### C. Example Configuration -An example configuration for this setup is shown below. - -![Google SAS Configuration Screen](https://i.imgur.com/9G0bymT.png) - -*The configuration screen is a right-hand sidebar next to the map view, hence the unwieldy aspect ratio.* - -Explanation of parameters: - -1. CBSD Category (A or B): - * Defined by rules in Section I.C above -2. User ID - * Specified by the SAS provider when you register -3. FCC ID and Serial Number: - * Both the radio and antenna model must be pre-authorized for use with CBRS by the FCC. - * The FCC ID is used to identify this approved device type. - * The serial number specifies the exact device identity. - * Both can usually be found on the outside of the device (circled in image below). -![](https://i.imgur.com/YDAABLk.jpg) - -4. Beamforming Gain, Beamwidth - * Based on antenna specs in II.B -5. EIRP - * [Effective Isotropic Radiated Power](https://www.everythingrf.com/rf-calculators/eirp-effective-isotropic-radiated-power) of your system including both the base station radio and antenna. - * For a Cat B CBSD, this must be 46 dBm/10 MHz=36 dBm/MHz or lower. - * Calculate this value by adding the max transmit power (actually power density per MHz) of the base station, in our case 28 dBm, to the antenna beamforming gain, in our case 18 dBi; 28+18=36 dBm/MHz. - * For the units requested by the Google interface, add 10 to this value to specify power per 10 MHz instead of per MHz. -6. Height - * Specified in terms of height Above Ground Level (AGL) which you can measure using a rangefinder/ measuring tape/ building plan, or in height Above Mean Sea Level (AMSL). - * **Not** in terms of HAAT as in the Cat A/B definition. - * Must be accurate to within 3 m. -7. Azimuth - * Refers to the compass heading/ direction that the antenna is pointing (set this to 0 for an omnidirectional antenna). - * This [FCC tool](https://www.fcc.gov/media/radio/distance-and-azimuths) is extremely helpful for calculating the azimuth based on the antenna’s gps location and that of a structure you are pointing it at. - * You can get these GPS coordinates via Google Maps or Google Earth. - ![](https://i.imgur.com/SgxORTx.png) - -8. Air Interface - * E_UTRA is the LTE radio standard used by our Baicells box. - * The only “supported spec” currently available for Baicells is FFS (according to a forum post, linked here). -9. Location: - * In the Google interface, set the site location in GPS coordinates under the tab labeled with the map pin icon. *(not shown)* -10. Parameters under "CBSD Info" - * Call Sign - * As far as I can tell, this can be any reasonable alphanumeric string as long as it is unique and matches the value of the “call sign” parameter as sent over by the eNB or domain proxy. - * You will set this in the SAS interface as well as either the eNB or Baicells Cloud Core (they all need to match). - * Others - * These should match the settings with the same name on the eNB’s local management portal, shown on the “Basic Info” page in section IV.A below. - -### D. CPI Signature -When the parameters are all filled out, click the big red “Ready for CPI” button at the bottom of the panel (not shown here). On the CPI’s version of the interface, it will provide a place to “sign” the configuration with their CPI certificate, which they will upload to the interface. **This must happen before the device can get a spectrum grant.** - -### E. Status Tab -After the CPI signs the eNB configuration, under the “Status” tab (visible in the config panel), you should see “Not yet Registered” (or a similar message) because the eNB has not checked in to the Google SAS yet with its matching parameters to complete the multi-step process. If something has otherwise gone wrong, you’ll see an error message here. - -### F. Other helpful links -* [Google CBSD registration and deregistration](https://support.google.com/sas/answer/9539493?hl=en&ref_topic=9455755) -* [Elevation finder tool with map](https://www.freemaptools.com/elevation-finder.htm) - -## III. Steps in Baicells Cloud interface - -### A. Make a Baicells OMC account. -Due to Baicells’ use of a “domain proxy” for their SAS requests, you will need to make a new user account in the Baicells Operators Management Console (OMC): https://cloudcore.baicells.com:4443/ - -This is distinct from their paid “Cloud Core” service which we will not be using in this tutorial, although the management portal is the same. - -### B. Take note of the CloudKey -Once you have made an account, note the 6-letter “CloudKey” in the upper right corner of the screen (circled in red). - -![](https://i.imgur.com/thYx40F.png) - - -This will need to be inputted into the local eNB management portal for the eNB to check into the Cloud OMC. - -*On your version of this portal, if you’re doing this for the first time, you shouldn’t see any eNBs already present.* - -### C. Set your SAS service provider. -Navigate to Advance→SAS in the left hand menu, and then click the gear icon on the upper right corner, which has the hover text “Settings.” - -## IV. Steps in Baicells management interface - -### A. Local Management Portal -The Baicells eNodeB (eNB) is best managed through the browser-based management portal; the current command line interface is accessible but extremely limited. - -The default IP address of the management portal (and that of most Baicells equipment I’ve seen) is 192.168.150.1, and the default login credentials are admin/admin. *I would recommend changing the admin login credentials to be more secure.* - -Connect your computer to the eNB via Ethernet, and navigate to this IP address in your browser (using http://192.168.150.1, not https). - -Baicells Initial Login Screen: - -![](https://i.imgur.com/4RlDG7R.png) - -BTS Info→“Basic Info” Page visible upon login: - -![](https://i.imgur.com/AdSEjMC.png) - -### B. Upgrade firmware -Upgrade the firmware to the latest firmware version that supports SAS functionality, or verify that it is already up to date. - -You can check the official [firmware page](https://na.baicells.com/Service/Firmware) under the correct eNB model. The Nova 233 CBRS small cell we’re using is model mBS1105. The latest firmware version after which SAS is officially supported is BaiBS_RTS_3.6.6.IMG (as of Feb 2021), for which the direct download is available [here](https://img.baicells.com//Upload/20200912/FILE/6aaf03fa-7768-40eb-8e1b-ee79f5cb443a.IMG). - -![](https://i.imgur.com/OPL0HYs.png) - -***Do not skip this step, otherwise none of the following steps will work right.*** - -### C. Get everything connected - -Once the firmware is upgraded, you will want to get the eNB connected to your local [LTE core network (EPC)](https://hackmd.io/brHS3l1-T_uTUaDEAHMOxw?view) as well as to the Internet so it can contact the necessary SAS infrastructure. - -#### 1. Configure Internet Access (WAN) -Navigate to the Network→WAN/LAN/VLAN tab on the left hand menu. - -We will set the WAN interface IP address to 192.168.151.1, since the Baicells console requires (for whatever reason) a different subnet for the WAN as opposed to the LAN. - -Then we will connect the eNB to an Ethernet port on the EPC that has the IP address 192.168.151.2 (as set up in our previous tutorial), which will act as the eNB’s Internet gateway. - -**Don’t forget to hit “Save” after each change you make in this interface.** -![](https://i.imgur.com/knqF26j.png) - -#### 2. Check Internet access - -At this point, if the EPC is configured correctly to pass eNB traffic to the Internet, the eNB should be able to ping an arbitrary IP address. - -To test this, navigate to the Network→Diagnostics tab on the left hand menu and select “Ping” under the “Method of Diagnostics” dropdown menu. Set the “Target IP Domain” to be a highly reachable IP address on the Internet such as 1.1.1.1, which is the CloudFlare DNS server. Press “Implement.” - -If the result is “Fail!” as in the screenshot, there is likely something wrong with your eNB’s Internet connection through the EPC; you should fix this issue before continuing. -![](https://i.imgur.com/tOgPFxa.png) - -#### 3. Reboot as needed -If a message appears that the eNB needs a reboot after the new settings are saved, navigate to the Reboot tab in the left hand menu and perform the reboot (Warm Reset is fine). - -#### 4. Attach to Baicells OMC -To configure the eNB to talk to the OMC as discussed in the prior section, navigate to the BTS Setting→Management Server tab in the management console and enter the CloudKey. - -Within a few minutes, the eNB should appear in your Baicells Cloud OMC console, and the “Basic Info” page should show that the OMC is “Connected.” -![](https://i.imgur.com/we7ySwo.png) -![](https://i.imgur.com/Ic7u0II.png) - -#### 5. Disable IPsec -For our purposes we will not be using IPsec between our EPC and eNB; the default IPSec configured is used for the Baicells Cloud EPC which we are not using. - -Navigate to the Network→“MME&IPSec Binding” menu tab and set “IPSec Status” to “Disable.” You may also delete the IPSec tunnels as shown below. -![](https://i.imgur.com/1XO8wj5.png) - -#### 6. Disable GPS Sync when testing indoors. -Navigate to the “BTS Setting”→“Sync Setting” menu and disable both “Forced Sync” and “GPS Sync Switch,” in case you need to work with the base station in a location where you don’t have a strong GPS signal. - -Some base stations will not start up normally or attach to the EPC unless they get a GPS signal, and we should avoid this behavior. -![](https://i.imgur.com/RPq0kzq.png) - -#### 7. Change the MME settings -Change the MME settings. Since we are using our local EPC, we will need to change the MME settings to reflect our MME’s IP address, on which it is listening for eNBs to attach, as well as other configurations. Navigate to the BTS Info→Quick Setting tab on the left hand menu. - -![](https://i.imgur.com/owYmUoJ.png) - -* Disable RF - * You should set the “RF Status” setting to “Disable” before you change the MME IP, because attaching to the MME will normally cause the eNB’s radio to turn on. - * Since we have not enabled the eNB to ask for spectrum coordinated by the SAS yet, turning on the radio may cause unwanted interference on someone else’s network. -* PLMN setting - * Remove the existing “PLMN ID” (by clicking the trash can symbol) and set it to the value that you have configured in your EPC. - * In our networks, we use “91054” as our PLMN, so add this as a “Primary” and “NotReserved” PLMN by entering the number in the text box and clicking the “+” button. -* MME IP address - * Remove the existing MME IP associated with the old PLMN. - * Add the new MME IP address, in our case 192.168.150.2, by entering it in the text box and clicking “+”. This MME IP should be associated with the newly added PLMN by default. - * Save the changes and reboot the eNB (Warm Reset); after the reboot has finished (within a few minutes), the eNB should attach to the MME. - * If you navigate to BTS Info→Basic Info, you should see the MME Status change from “Not Connected” to “Connected.” If you are looking at the MME logs on the EPC, you will also see the record that an eNB has attached. - -#### 8. Enable SAS -**SAS should only be enabled after successfully attaching the eNB to the MME.** Unfortunately, when SAS is enabled, the eNB will not attach to the MME unless it has a currently valid authorization to transmit on a certain frequency. However, until it is attached to an MME, the Baicells Cloud OMC will not provide it this authorization. - -So we need to have SAS disabled first with the RF also disabled, attach the eNB to the MME, and then enable SAS. - -Choose “Multi-step” under “SAS Registration Type,” as specified in Section I.E. Also choose “B” under “category,” and write in the other parameters to match the ones with the same name in the Google SAS configuration. - -![](https://i.imgur.com/d5KzckP.png) - - -After you click “Save,” SAS should be enabled immediately. You should see the SAS enabled status change in the Baicells Cloud OMC. If all goes smoothly, your device should get an authorization to transmit within a few minutes and the radio should turn on! - -#### 9. Check Baicells CLOUD OMC to debug issues -You can check the status of the SAS authorization process in the Cloud OMC. Here you can find logs (upper right corner of SAS screen, shown in the screenshot below) with any error messages that may have occurred in the process. -![](https://i.imgur.com/rQntnQ9.png) - -* Errors can be caused by invalid or non-matching parameter values, lack of CPI signature, lack of spectrum availability, etc. -* In more difficult cases, after device registration the SAS may not respond to spectrum inquiries without sending any clear error messages. I have encountered this scenario when requesting spectrum around midnight, which may have been caused by brief database unavailability during the daily “SAS Sync” or IAP. My recommendation is to avoid requesting a new spectrum grant after 11 pm PST. -* If you change anything about the equipment used on site or the location/orientation of the equipment, you need to change the SAS registration, have it re-signed by the CPI, and use the Baicells OMC to re-request a new spectrum authorization- this process is described in the following section. - -## V. How to change location, antenna properties, etc. after deployment -As an example, this section will show how you would change the equipment’s location upon moving from test site to deployment site. - -1. Get the new GPS location either manually using Google Maps/Earth, or automatically using Baicells OMC’s GPS reading for the eNB if available. -2. Google SAS steps - * In the upper right hand corner of the Google SAS configuration for the deployed equipment (long narrow right side panel for a particular site), press the unlock button (shaped like a padlock) to make the configuration editable. -
- - ![](https://i.imgur.com/B6DB1or.png) -
- - * To edit the site location, click on the map pin icon in the upper left corner of this same right hand configuration panel to enter the location panel. Enter the new GPS coordinates in the box. After your changes, lock the site configuration again. (If the red “Ready for CPI” button appears again at the bottom of the main configuration panel, go ahead and click it to prompt the CPI to sign.) - -
- - ![](https://i.imgur.com/mFD3cWc.png) -
- - * You may have to wait a few minutes or hours for the changes to sync to the CPI’s SAS database view. If after a while the CPI still cannot see the location change, ask them to enter the new GPS coordinates in their own interface and re-sign the configuration. -3. Baicells Cloud OMC steps - * On the Baicells OMC, navigate to the Advance→SAS screen where you can see the list of CBRS devices and their SAS status. Click on the 3 dots ( ⠇) symbol before the serial number for a particular device and click on “Procedure” to enter the SAS procedure screen. - ![](https://i.imgur.com/6zXTVah.png) - - * On the Procedure screen, you can see the most recent SAS logs, relinquish and re-request active spectrum authorizations, or de-register and re-register devices. First click on the “Authorized” icon and click on the “Relinquishment req” button to relinquish the current spectrum authorization. Then the latter two icons will become greyed out, but the device will remain registered. - ![](https://i.imgur.com/bjbGzyY.png) - - * We will need to fully de-register and re-register the device with the new parameters. Click the “Registered” icon and then the “De-register” button when it appears to de-register the device. - ![](https://i.imgur.com/aW9qDZF.png) - - * Once the device is in the “Unregistered” state, click the “Unregistered” icon and then click the “Register req” button when it appears. If all goes well, the device should re-register, and also request and receive a new grant (completing the full procedure) within a few moments. diff --git a/docs/infrastructure/software.md b/docs/infrastructure/software.md deleted file mode 100644 index efbc19a..0000000 --- a/docs/infrastructure/software.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Software Overview ---- - -# Our Software - -Here is a list of the software that we use to deploy, maintain, and plan our network sites. - -## Networking - -### Local Services -We use the [CoLTE project](https://github.com/uw-ictd/colte) maintained by the University of Washington [ICTD Lab](https://ictd.cs.washington.edu/) -to provide services such as network monitoring, web-based administration, and local web and DNS serving/caching. - -### Evolved Packet Core (EPC) -Our EPC is powered by [Open5GS](https://github.com/open5gs/open5gs), an open-source project for 4G and 5G core networks. Currently all of our networks are 4G networks. - -### Spectrum Access System (SAS) -We have a partnership with [Google SAS](https://www.google.com/get/spectrumdatabase/sas/) to gain access to CBRS spectrum. - -Learn more about our SAS setup [here](sas-setup.md). - -### Network Monitoring and Alerting -We use [LibreNMS](https://www.librenms.org) and SNMPd to monitor our nodes and provide alerting. Our Baicells-specific Network Manager setup is documented [here](librenms-manager-setup.md), and our instructions for configuring a new node can be found [here](librenms-setup.md). - -## Field Measurement - -### Network Performance Measurement Tool -The LCL Network Performance Measurement Tool is an Android App in development that will measure a variety of network metrics, including but not limited to ping, upload/download speed, signal strength. We will use this tool to easily capture and upload network metrics -in the field so that we can provide better estimates of what kind of Internet access that our users can expect to receive. - -### Network Cell Info Lite -[Network Cell Info Lite](https://play.google.com/store/apps/details?id=com.wilysis.cellinfolite) is an Android App on the Google Playstore that is free to use (with advertisements) -and is capable of taking network metric measurements and recording them to upload. This is an option that we -use but are not satisfied with for many reasons, which is why we are developing our own app. - -## Site Planning - -### Google Earth -We primarily use the Google Earth Pro [desktop application](https://www.google.com/earth/versions/#earth-pro) to do a rough line-of-sight evaluation. We perform what is called a "viewshed analysis" that allows us to determine what is visible from a specific point on Earth (e.g. a rooftop). - -### Ubiquiti Line of Sight -A [web-based line of sight tool](https://link.ui.com/) provided by Ubiquiti that contains helpful altitude data and diagrams. -A drawback is that it is specialized to provide data for Ubiquiti devices only. - - -## Other resources -### Facebook ISP Toolbox Line of Sight -A [web-based line of sight tool](https://www.facebook.com/isptoolbox/line-of-sight-check/) provided by Facebook Connectivity that utilizes public LiDAR data. Unfortunately LiDAR data for the Seattle area is not present yet, although there is data for some areas in Tacoma. - -### Facebook ISP Toolbox Market Evaluator -A [web-based market evaluator](https://www.facebook.com/isptoolbox/market-evaluator/) provided by Facebook Connectivity that can be used -to provide more context about the areas around potential network sites. It offers information about other service providers in the area, average household income, median speeds, and current lowest broadband price available. diff --git a/docs/learn/.pages b/docs/learn/.pages deleted file mode 100644 index 23e333f..0000000 --- a/docs/learn/.pages +++ /dev/null @@ -1,5 +0,0 @@ -nav: - - wireless-communication.md - - cable-crimping.md - - lte-networks.md - - networking.md \ No newline at end of file diff --git a/docs/learn/cable-crimping.md b/docs/learn/cable-crimping.md deleted file mode 100644 index 69ee336..0000000 --- a/docs/learn/cable-crimping.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -title: Crimping Ethernet Cables ---- - -# Crimping Ethernet Cables - -In this article, you'll all about crimping ethernet cables! -### What is crimping an ethernet cable? -Crimping an ethernet cable is the process of attaching connectors onto the ends of ethernet cables. This process is also called 'RJ45 crimping' because RJ45 is the name of the connectors that are used for ethernet cables, and they are what is being crimped. - -### Why? -Setting up networks involves setting up long ethernet cable connections between different devices. Instead of buying premade ethernet cables of varying lengths (e.g. 5ft, 10ft, 50ft, etc.), it's more practical to just have a big spool of cabling that we can roll out and cut to the exact length we need. - -Therefore we need to be able to attach RJ45 connectors to the ends of these cut cables so that we can actually plug them in! - -## Crimping Kit - -Here are some tools you should have in your crimping kit! - -### RJ45 Crimping Tool - -![RJ45 Crimping Tool](../assets/cable-crimping/kit-crimping-tool.jpg) - -An RJ45 crimping tool is the most essential tool. Although it's technically possible to crimp ethernet cables without this specialized tool, it's [not very practical](https://youtu.be/ysYLdFxNVuc) for crimping lots of cables. - -Its primary utility is to do the actual 'crimping' part of compressing/crimping the tiny gold pins in the RJ45 connector onto the ethernet cables. It also has blades that can be used to cut or strip wires. - -### Cable Stripper - -![Cable Stripper](../assets/cable-crimping/kit-cable-stripper.jpg) - -Cable strippers are used to take off the protecting shielding around cables and expose the inner wires. You can also do the same thing with a simple blade or pair of scissors. The trickiest part about stripping cables is trying to avoid cutting the inner wires! - -### RJ45 Connectors - -![RJ45 Connector](../assets/cable-crimping/kit-rj45-connector.jpg) - -RJ45 connectors are required for crimping because they feature the 8 golden pins that get crimped onto the 8 wires of the ethernet cable. They are what get plugged into ethernet ports! They also feature a latch/clip that locks the ethernet cable into the port once it is plugged in. - -### RJ45 Boots - -![RJ45 Boot](../assets/cable-crimping/kit-rj45-boot.jpg) - -RJ45 boots can be optionally used to protect the RJ45 connector. It provides insulation and prevents the cable from being breaking easily. They have to put slipped onto the cable before you put on the RJ45 connectors though! - -### RJ45 Cable Tester - -![RJ45 Cable Tester](../assets/cable-crimping/kit-cable-tester.jpg) - -RJ45 cable testers allow you to guarantee that you did the job correctly! - -They have two pieces that separate from each other, and you plug each end of your crimped ethernet cable into the port on each piece. Then you turn it on and the cable tester will test the connection for all 8 pins. If there are any missing lights on any of the pins, it means that you messed up somewhere and have to restart! - -## How to Crimp an Ethernet Cable - -Assuming you have a crimping kit and an ethernet cable that needs to be crimped, here are all the steps! - -### Step 0) Slip on the RJ45 boot (optional) - -![2 boots on a cable](../assets/cable-crimping/0-boots.jpg) - -### Step 1) Strip the cable - -![cable being stripped by cable stripper](../assets/cable-crimping/1-strip.png) - -- Push the cable into the razor slot of the strip tool and turn it around the cable to make an even cut around the sheath. Careful not to nick the wires inside! -- Unwrap the blue foil shielding and plastic to uncover the twisted wire pairs. -- Push the copper grounding wire to the side. (Ignore the white string.) - -### Step 2) Organize the wires - -In this step, you'll be taking the 8 colored wires inside the ethernet cable and putting them into the correct ordering of colors. - -**NOTE**This is the hardest part of crimping! The wires are small and are hard to control. Take your time and make sure you do this step correctly! Otherwise you might have to go back and restart. - -### Step 2.1) Untwist the wires - -![4 twisted pairs becoming 8 individual wires](../assets/cable-crimping/2.1-untwist.png) - -There should be 4 pairs of wires: green, brown, orange, and blue. Each pair has a solid-colored wire and a striped-colored wire. Untwist these pairs and separate them into the 8 wires. - -### Step 2.2) Straighten out wires - -After untwisting the wires, they are probably still kinked and look like they want to be twisted. In this step, you should carefully grab all the wires and try to straighten them out by pulling on them. This will prevent the wires from moving around later on. - -**WARNING** Don't break off the wires! - -### Step 2.3) Lay out wires in order - -![Diagram showcasing ordering of RJ45 wires](../assets/cable-crimping/2.3-color-diagram.png) -![8 wires in RJ45 color order](../assets/cable-crimping/2.3-ordered.png) - -With your straightened out wires, put them into the correct order! Make sure that the wires are all flat and in line with each other. - -The ordering for these wires is: -1. Striped orange -2. Solid orange -3. Striped green -4. Solid blue -5. Striped blue -6. Solid green -7. Striped brown -8. Solid brown - -**TIP** After laying them out in order, straighten them out again as a group! This will help keep the wires together. - -### Step 2.4) Trim the wires - -![trimmed wires](../assets/cable-crimping/2.4-trim.png) - -Trim the wires evenly to about 1/2 inch in length using scissors or the blade of your crimping tool. You want to make sure you have enough room for the wires to reach the end of the RJ45 connector. But also try to have room for the shielding of the cable to be inserted into the connector too. - -**TIP** You can put the wires side-by-side to the RJ45 connector to see how long you should cut it. Look at the next step to see what the final product looks like. - -**TIP** If you don't have the shielding inside of the connector, it makes it easier for the wires to snap off later, which is bad. - -**TIP** Make sure that you cut the wires evenly! - -### Step 3) Slide wires into RJ45 connector - -![Wires inserted into RJ45 connector with clip-side down](../assets/cable-crimping/3-diagram.png) - -Carefully slide your 8 wires into the connector. Make sure that the clip is facing away from you! If it is really hard to slide it into the connector, you probably didn't straighten out the wires enough in step 2.2 or 2.3. - -**MORE INFO** Inserting the wires with the clip facing away from you is the standard. However, you could technically do it in 'reverse' and insert the wires with the clip facing you, as long as you do it on both ends of the cable. You shouldn't do this in practice though because others would get confused when looking at your cable. - -### Step 4) Crimp it - -![Inserting into crimping tool and crimping](../assets/cable-crimping/4-crimp.png) - -Push the RJ45 connector into the slot of your crimping tool for RJ45 connectors. The slot should be labeled something like "8P" for the 8-pin RJ45 connector that you're using. - -In this step, you're doing the actual 'crimping' part and crimping/compressing/stabbing the 8 golden pins on the RJ45 connector into the 8 colored wires. - -**TIP** Squeeze as hard as you can! You need to make sure that all 8 pins are crimped. - -### Step 5) Test it - -![Testing with cable tester, green light showing](../assets/cable-crimping/5-test.png) - -Slide the two pieces of the tester apart and plug each of the cable ends into either piece. Turn the switch to “On” or “Slow.” If it's working, all 8 numbers should be flashing green. - -If any of them are not showing green, it means something is wrong and you have to redo it! The RJ45 connector can't be reused once it's crimped, so you should just cut the end off and start back at step 1. - -If everything is green, then you're done! If you had a cable boot, you can push the boots onto the RJ45 connector now. - -## Resources - -### Workshop Slides -- [ISOC ICS Training Workshop](https://docs.google.com/presentation/d/1HG5OcJysTicr_JHOlsKTB2ewWBVexrQDYv0Xn3_1hYA/edit?usp=sharing) - -### Videos -- [Crimping Tutorial (2 mins)](https://www.youtube.com/watch?v=WvP0D0jiyLg) -- [Cable Testing](https://youtu.be/3tHvOLBp2zM) - - Only need first 7 minutes for the basics - -### Websites -- [Color Coding Diagrams](https://incentre.net/ethernet-cable-color-coding-diagram/) -- [Crimping Comic](https://raw.githubusercontent.com/sudomesh/propaganda/master/handouts/zine-assets/crimp_comic.png) - - From People's Open Network + sudomesh - -### Shopping -- [Crimping Kit ($23)](https://www.amazon.com/SGILE-Ethernet-Maintenance-Connector-Accessories/dp/B01L924436/) - - Comes with a nice case - - Might need to buy your own batteries for cable tester -- [Crimping Kit ($17)](https://www.amazon.com/UbiGear-Crimper-Connector-Network-Crimper315/dp/B008UY5WL0/) - - Might need to buy your own batteries for cable tester diff --git a/docs/learn/lte-networks.md b/docs/learn/lte-networks.md deleted file mode 100644 index 453b2e6..0000000 --- a/docs/learn/lte-networks.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: LTE Networks ---- - -# LTE Networks - -Our network uses a 4G LTE network architecture. Understanding everything is a huge challenge, but understanding it at a high-level is very achievable. - -Consider exploring all of these links as they all complement each other well! - -## Helpful videos - -- [How Cell Service Actually Works](https://www.youtube.com/watch?v=0faCad2kKeg) - - Broad overview of cellular technologies, from the very basics up to advanced topics, in only 20 minutes -- [AT&T Archives Video: AMPS](https://www.youtube.com/watch?v=d6X_1PcR_gs) - - Old video from 1978 about an older version of cell networks (AMPS) - - Very very high quality and gives good background on cellular networks -- [Learn 4G LTE Network Architecture](https://www.youtube.com/watch?v=-nDjkV-NulM) - - Quick high-level overview of 4G LTE architecture - - Only shows diagrams -- [How does your mobile phone work?](https://www.youtube.com/watch?v=1JZG9x_VOwA) - - Very good visual for the entire system of how cell phones work today - - Has 3d animation so it's easier to understand conceptually - - Doesn't go over LTE architecture specifically -- [Driving Factors of LTE Architecture](https://youtu.be/aGRTBA1tYRo) - - Gain more understanding of the context around LTE architecture - -## Helpful articles - -- [YaleBTS LTE Concepts](https://yatebts.com/documentation/concepts/lte-concepts/) - - Explains basically ALL the parts of the LTE architecture -- [TutorialsPoint LTE Network Architecture](https://www.tutorialspoint.com/lte/lte_network_architecture.htm) - - Gives overview of LTE architecture - - Not very detailed, but it's a good introduction to the individual components of LTE -- [Open5GS Introduction](https://open5gs.org/open5gs/docs/guide/01-quickstart/) - - Documentation for Open5GS, the repo that our networks rely on - - This is the most applicable to our specific network because it's what we actually use. diff --git a/docs/learn/networking.md b/docs/learn/networking.md deleted file mode 100644 index 5bdcd4e..0000000 --- a/docs/learn/networking.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Networking ---- - -# Learn about Computer Networks - -This page is currently in development. For now, please view our lesson on computer networks -from our Digital Stewards curriculum [here](https://docs.google.com/presentation/d/1efYpUrzymaL9CYRJ06KmrtZnZGhfYdAUahpo6TpdTNc/edit?usp=sharing). - -**TODO** diff --git a/docs/learn/wireless-communication.md b/docs/learn/wireless-communication.md deleted file mode 100644 index 92644b5..0000000 --- a/docs/learn/wireless-communication.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -title: Wireless Communication ---- - -# Crash Course in Wireless Communication - -Authored by: Dominick Ta -Last updated: June 18th, 2021 - -TODO: - -- [ ] Expand on transmit/receiving in antennas section -- [ ] Finish communication section -- [ ] Finish protocols section - -This is a crash course in wireless communication: the magical way that we humans send information (e.g. music, messages, text, videos) to each other over long distances, without a wire. - -Wireless communication is primarily powered by radio waves, a physical thing that we as humans cannot see or touch. In this article you will gain a layman's understanding of how wireless communication works. Specifically, this article will cover the following broad topics: - -- Radio waves and the physics behind it -- Antennas -- How data is communicated via radio waves -- Examples of wireless communication technologies - -This is not meant to be a comprehensive article, so there will be a lot of simplifications, analogies, and informal explanations. Please let me know at domta@cs.uw.edu if you see any inaccuracies, misconceptions, or misnomers. - ---- - -## Radio waves - -Radio waves are just oscillations of energy that exist throughout our environment. They can be generated naturally by lightning, or artifically by equipment made by humans such as cell phones. - -The technical definition of radio waves are that they are a form of 'electromagnetic radiation'. Other types of electromagnetic radiation include visible light (what we get from the sun), infrared, and X-rays. - -![Image of a radio surrounded by pulsating radio waves](https://cdn.britannica.com/s:800x450,c:crop/86/214986-138-D4D194FD/How-radio-works-overview-radio-waves-frequency-amplitude-modulation.jpg) - -### How radio waves are created - -To understand why radio waves are considered a type of electromagnetic radiation, it is helpful to know how we generate radio waves. ***This section is not important to fully understand, but it is good to skim to have a basic idea of how this stuff works.*** - -We create radio waves by **moving electrons back and forth** within an electrically conductive object (an object made of material that allows electrons to move freely, such as metals). - -This works because electrons have special properties: - -- All electrons have '[electric fields](https://en.wikipedia.org/wiki/Electric_field)' surrounding them that attracts and repels other charged particles. -- All moving electrons produce '[magnetic fields](https://en.wikipedia.org/wiki/Magnetic_field)' that are the basis for how magnets work. -- Therefore, when electrons move there are two fields (electric & magnetic) present that combine to create creates an '[electromagnetic field](https://en.wikipedia.org/wiki/Electromagnetic_field)'. - -By moving an electron back and forth in an oscillating (repetitive) motion, these electromagnetic fields are constantly being disturbed/moved in a way that creates **electromagnetic waves** or equivalently, [electromagnetic radiation](https://en.wikipedia.org/wiki/Electromagnetic_radiation). - -In this GIF below, we see the charge of an antenna changing, representing electrons moving back and forth within the antenna. This creates pulsating electromagnetic waves. **Note: in this particular GIF, it is actually only showing the pulsating electric fields.** - -![Animated image of waves pulsating from an antenna](https://upload.wikimedia.org/wikipedia/commons/a/a6/Dipole_xmting_antenna_animation_4_408x318x150ms.gif) - -In this GIF below, we are able to fully see an electromagnetic wave with a 3D representation. The red component of the wave represents the electric field, while the blue component represents the magnetic field. Notice how they are perpendicular to each other! *This is also why the GIF above didn't show both fields: it was a 2D representation!* -![3D gif of electromagnetic waves, showing both the electric fields and magnetic fields](https://upload.wikimedia.org/wikipedia/commons/4/4c/Electromagneticwave3D.gif) - -In this section, we learned about radio waves. In essence, you can just think of radio waves as physical phenomenon that look like these sine waves: -![GIF of a basic sinusoidal wave at a constant frequency](https://i.gifer.com/7myX.gif) - -With the proper hardware and knowledge, we can create any type of wave we want! We can vary frequencies (how fast the waves go by), amplitudes (how tall/powerful the waves are), and phases (what position within the loop the wave is in). - -### Frequency & Wavelength -It turns out that a fundamental characteristic of any given radio wave is its frequency. Therefore, it's important to know what 'frequency' means, and how it relates to the concept of 'wavelength'. - -[Frequency](https://en.wikipedia.org/wiki/Frequency) is a measurement of how often something happens. In the case of radio waves, it measures how many cycles of a radio wave occurs in a certain amonut of time. Frequency is measured in the units of **hertz (Hz)**, which represents cycles per second. If we had a radio wave that passed through 10 complete cycles in a minute, it would have a frequency of 0.16Hz (10 cycles per 60 seconds). - -[Wavelength](https://en.wikipedia.org/wiki/Wavelength) measures the physical length of a single cycle in a radio wave. If we could see radio waves and measure it, and we saw that there was a distance of 10-feet between two of the peaks in a radio wave, then that radio wave would have a wavelength of 10 feet. - -![Image showcasing a wave with high frequency & short wavelength, and a wave with low frequency & long wavelength](https://cdn.britannica.com/83/194283-004-37696A2F.jpg) - -In the image above, we can see a natural relationship between frequency and wavelength: a faster frequency means that you will have shorter wavelengths! Or conversely, longer wavelengths means that there is a slower frequency! When one value goes up, the other value has to go down; this is called an **inverse relationship**. - -So if I told you that I had a radio wave with a very high frequency, you could figure out that my radio wave has very small wavelengths. And actually, if I told you exactly what frequency my radio waves travelled at, you'd be able to figure out the exact size of the wavelength! - -For example, if I told you I had a radio wave with a frequency of 10Hz, you would be able to figure out that my radio wave has a wavelength of approximately 30,000,000 meters. - -This is because radio waves are a physical thing, so they always travel through air at the same speed: the speed of light. This means we can consistently convert back and forth between frequency and wavelength with some basic algebra. - -The basic equation, where c is the speed of light (3.0 x 10^8 m/s), is: -frequency (Hz) * wavelength (m) = c - -So given a radio wave with a frequency of 10Hz, to solve for wavelength, I just need to take the speed of light and divide by 10! 30,000,000 divided by 10 gives a value of a wavelength of 30,000,000 meters. - -To summarize this section, frequency and wavelength are important properties of radio waves. Although they describe different aspects of a radio wave, they are essentially synonymous because we can easily convert between the two. - -When people are talking about radio waves, you may hear them talk in terms of frequencies (e.g. megahertz, gigahertz) or you may hear them talk in terms of wavelengths (e.g. meters). - -In the next section, we'll see why exactly frequency is such an important characteristic of radio waves. - -### How we distinguish between radio waves -An important thing to remember about radio waves is that nowadays they **surround us everywhere** we go! Radio waves are powerful because they can **go through obstacles** like walls, and they can potentially propagate over **huge distances** at a relatively cheap cost. - -Radio waves are used to communicate information in technologies such as GPS, WiFi, Bluetooth, cell phones, music radio stations, and more. - -The problem with this is that these radio waves interfere with each other and combine together to become a jumbled mess! They are all co-existing within the same space, and they can't avoid each other. - -In real life we don't get to have nice, simple, isolated radio wave like in the previous sections, we get a mix of radio waves coming all at once! And somehow, we have to find and narrow down the signal we care about. - -![GIF showcasing a bunch of random radio waves](https://miro.medium.com/max/1614/1*OyERBXcdPThv79uGu0QYGw.gif) - -Imagine you are at an airport and you're trying to talk to your friend, but it is super crowded and theres people talking and shouting all around you. It would be super hard to hear your friend and have a conversation! - -*(In this situation the people represent devices that use radio waves, the sound waves represent radio waves, and the voices & words represent the data we want to transmit)* - -But if your friend talks loud enough, even though your ear is full of noise from everyone else in the airport, you can still figure out what your friend is trying to tell you. This is because you're smart enough to recognize what your friend's voice sounds like and you can focus on that voice. We can do the same thing with radio waves of different frequencies! - -*(Being able to focus on your friend's specific voice is analogous to being able to focus on only radio waves of a specific frequency)* - -In the image below, we can see a red radio wave. This radio wave is actually the combination of 5 radio waves of different frequencies! In the blue, we see this same signal analyzed and split into the individual components, located at its respective frequencies. *(Analogy: 5 people talking at the same time, what your ears hear is the red. Your brain recognizing that these are 5 different voices is the blue)* -![Two graphs, top graph is a red sinusoidal signal in the time domain, bottom graph is that same signal in the frequency domain showcasing 5 peaks at frequencies of 10, 20, 30, 40, and 50Hz.](https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/FFT_of_Cosine_Summation_Function.svg/440px-FFT_of_Cosine_Summation_Function.svg.png) - -The mathematical process of going from the red representation (the signal in the time domain) to the blue representation (the signal in the frequency domain) is called the "Discrete Fourier Transform". *You may also hear about something called the "Fast Fourier Transform" which is the discrete fourier transform, but a very clever and fast way to calculate it.* - -*(Intuitively, the DFT/FFT is essentially comparing a bunch of sine waves of varying frequencies to the signal detected, and calculating how similar they are. This would be like your brain iterating through all possible human voices and checking to see how strongly your ears hear that particular voice)* - -**This means that even though there may be a jumbled mess of radio waves surrounding us at all times, if they are radio waves of different frequencies, we are able to distinguish between all these different frequencies using some old fashioned engineering.** - -This non-trivial fact is what allows our world to have so many different types of devices communicating wirelessly all at the same time! They are all using radio waves, all in the same space, but at different frequencies! This is why radio waves are identified by their frequency (or wavelength). - -### Summary - -In this section on radio waves, we learned about what they are, the basics of how they're produced, frequency & wavelength, and how we distinguish between different radio waves co-existing in the same space. These are the fundamental concepts behind the physics of radio waves that engineers take advantage of. - -In the next section on antennas, we will learn what they are and how they are used to efficiently propagate/send radio waves. - -In another section, we will learn more about how exactly we manipulate radio waves to convey the information we want to send. - -## Antennas - -We use antennas in our everyday lives, but most people don't know how they work. We use antennas on cars, on buildings, and even within our computers and phones! - -### How they work - -#### Material properties - -Antennas are all made up of electrically conductive material. This usually means antennas are made out of metals like copper. - -Materials that are electrically conductive allow electrons to freely move throughout it. The opposite type of material would be electrically insulating material. - -As a real-life analogy, imagine a typical swimming pool filled with water. A normal pool like this allows people to freely swim through it! In this analogy the water represents electrically conductive material and people represent electrons. Now imagine a piece of copper metal as being that pool of water. Because copper is electrically conductive, electrons can freely move through it! - -An analogy for an electrically insulating material would be a swimming pool filled with jello/pudding/gelatin. If someone tried diving into that pool, they wouldn't get very far and it'd be super hard or impossible to swim through it. In this analogy the jello/pudding/gelatin represents electrically insulating material and people represent electrons. Now imagine a piece of plastic being that pool of water. Because plastic is electrically insulating, eletrons are mostly stuck where they are inside the plastic! - -#### Taking advantage of moving electrons -Antennas need to be electrically conductive because they transmit and receive radio waves by taking advantage of the movement of electrons. - -In the above section on "How radio waves are created" we learned that radio waves are generated by moving an electron back and forth in an oscillating (repetitive) motion. This oscillation of electrons occurs in the antennas. - -If antennas weren't electrically conductive, these electrons wouldn't be able to move and create these radio waves! - -#### How transmitting with an antenna works - -In order to transmit with an antenna, the antenna needs to be connected to an electrical component that can control the movement of these electrons. - -TODO: expand/clarify? - -#### How receiving with an antenna works - -Antennas receive radio signals passively. As the electromagnetic fields are manipulated in the environment around an antenna, the electrons in the antenna move accordingly (because of how the physics of it work). By monitoring the movement of these electrons, the respective radio wave can be captured. - -TODO: expand/clarify? - -## Communicating with radio waves - -How do we as humans harness this power of manipulating radio waves as communication? We agree on a bunch of different rules and conventions on how we will manipulating these radio waves to convey information efficiently and responsibly. In the United States, most of these rules are established and enforced by the Federal Communications Commission (FCC). - -### An analogy -To lay a conceptual foundation for this relatively abstract section, here is an analogy. - -Suppose we have two friends with the simple names of "A" and "B" that want to talk to each other over a distance, but they cant see, hear, or touch each other over that distance. The only way method of communication they have is a really long piece of rope between them. So once they travel far away from each other, they will each be holding one end of the rope and will be trying to communicate. - -But before they travel far away from each other, they need to talk to each other to figure out a set of rules of communication so that they can understand each other when they feel the rope moving. - -When coming up with these rules for communication, one of the first things that A & B need to do is come up with a language. Because A & B really like computers, they chose the language of computers: binary. They chose this language because it is super simple and effective; it only has two letters (1 and 0) and you can send tons of information by being clever with 1s and 0s (thats what computers do). - -Now the next thing that A & B needs to do is figure out how to use the rope to send these 1s and 0s. Here are some ideas they brainstormed together: -* If the rope is moving up and down (oscillating), then that is considered a 1. If the rope is not moving, then that is considered a 0. -* If the rope is oscillating super fast then that's a 1, but if the rope is oscillating slowly then that's a 0. -* If the rope is oscillating super fast then that's a 0, but if the rope is oscillating slowly then that's a 1. -* If the rope is oscillating with a big height then that's a 1, but if the rope is oscillating with small height then that's a 0. - -A & B realized two properties of the movement of the rope that they could capture: (1) frequency, how fast the rope is oscillating, and (2) amplitude, how tall the rope is when its oscillating. - -The term for manipulating these properties is "modulation". This makes sense because a dictionary definition of modulation is "to adjust"; modulation is just a fancy word for changing. - -In this analogy, the material/medium of communication was the rope. But in our context, the material/medium of communication is radio waves. Both of these materials have frequency and amplitude that can be modulated to send information, and demodulated to receive that information. - -In the next section, we'll learn about frequency and amplitude modulation. We'll also learn about some other types of modulation that don't have a direct real-life connection to ropes. - -### Modulation - -#### Frequency modulation - -#### Amplitude modulation - -#### Other modulation schemes - -### Frequency allocation - -#### Baseband signal and carrier signals - -#### Bandwidth - -#### Bands and Channels - -## Wireless Communication Protocols - -In the previous section on "Communicating with Radio Waves", we learned how we are able to transmit messages wirelessly over radio waves via modulation. - -We understood this through our analogy with friends A & B who came up with a way to send letters in their binary language (1 and 0) through a rope. Assuming they were able to do this successfully, they could send long streams of messages that look something like "101011100101010101010". But that's still not enough! They need to come up with a 'protocol' for deciphering what this message actually means! - -For example, they could come up with a simple protocol for sending messages about how their day went. -* The first 3 letters would represent how their day went: "101" could mean that they had a good day, and "010" could mean that they had a bad day, and maybe "110" means that their day went okay. -* The next 3 letters would represent the weather: "100" could mean that it was sunny and "101" could mean that it was rainy. - -We have these same types of protocols in real life that are much more complicated. They are carefully designed to ensure security (can people intercept messages?), reliability (what if the message gets messed up in certain places?), and efficiency (how much useful data can we send at a time?). - -### WiFi - -### Bluetooth - -### Cellular communication diff --git a/faq/about/index.html b/faq/about/index.html new file mode 100644 index 0000000..fa2ed34 --- /dev/null +++ b/faq/about/index.html @@ -0,0 +1,191 @@ + + + + + + + + What is the Seattle Community Network? - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

About Seattle Community Network

+

Seattle Community Network (SCN) is a community network dedicated to providing fair access to underserved communities all across the Puget Sound. SCN is a project of Local Connectivity Lab, a 501(c)(3) registered non-profit that works to share free or low-cost broadband access in higher-need areas throughout the Puget Sound region, making use of existing network infrastructure such as buildings and fiber-optic cables to extend coverage to more people.

+

As a community network, we rely on the help of local residents such as yourself to maintain and grow the network. Joining us is a great way to become an active member of your own community, make friends, and learn valuable technical skills.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/faq/connection/index.html b/faq/connection/index.html new file mode 100644 index 0000000..5d6d177 --- /dev/null +++ b/faq/connection/index.html @@ -0,0 +1,211 @@ + + + + + + + + How do I get Internet? - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

How do I get Internet from the Seattle Community Network?

+

Eligibility

+

The Seattle Community Network exists to provide free or low-cost internet to low-income and in-need users.

+

We prioritize serving the following groups:

+
    +
  • low-income families of students
  • +
  • unemployed adults (looking for work)
  • +
  • majority non-English speaking adults/families
  • +
  • seniors
  • +
+

Registration

+

To connect to the internet through the Seattle Community Network, you will need to register with us.

+

To register, you can: +- Email lcl@seattlecommunitynetwork.org +- Contact us by phone at (253) 655-7221 and leaving a voice mail or text.

+

Hardware

+

Once your registration is processed, you will receive the necessary hardware to connect to the network.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/faq/help/index.html b/faq/help/index.html new file mode 100644 index 0000000..2da5f98 --- /dev/null +++ b/faq/help/index.html @@ -0,0 +1,195 @@ + + + + + + + + How can I Help? - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

How can I Help?

+

Volunteer!

+

SCN is run completely by volunteers. There are many ways you can help, and no technology skills are required. We need help with everything from setting up network hardware and developing software to community outreach and fundraising. If you want to help, we can use your talents!

+

First, make sure you get connected with our community.

+

Next, why not Join a Team or Contribute to SCN Docs?

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/faq/how/index.html b/faq/how/index.html new file mode 100644 index 0000000..ee23add --- /dev/null +++ b/faq/how/index.html @@ -0,0 +1,192 @@ + + + + + + + + How does SCN Work? - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

How Does the Seattle Community Network Work?

+

The Seattle Community Network partners with the University of Washington to share free or low-cost internet access with areas of higher need.

+

The Seattle Community Network (SCN) is a wireless Internet access network using 4G LTE and WiFi technologies, providing public access from partner locations such as libraries, schools, businesses, and community centers. The Internet connection at these sites is shared wirelessly to nearby devices using the 4G LTE (cell-phone) data standard, which can be used by certain phones and hotspots (also known as Customer Premises Equipment or CPE). Individual users can connect to this signal using SCN-provided (or other compatible) devices to create a local WiFi network in their home. Some of our installed sites utilize a portion of the University of Washington's internet bandwidth, sharing it out to further neighborhoods via point-to-point wireless links. Some sites use other upstream internet service providers (ISPs) such as Lumen.

+

The network is completely created, managed, and maintained by volunteers with a range of diverse skills in information technology and beyond. All infrastructure is paid for by generous donations from sponsors and the public. Speaking of which, why not volunteer or donate?

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/faq/site/index.html b/faq/site/index.html new file mode 100644 index 0000000..687026c --- /dev/null +++ b/faq/site/index.html @@ -0,0 +1,192 @@ + + + + + + + + What is this site? - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

About This Website

+

The Seattle Community Network Docs website is the central hub for information about our community and networks. Here, we describe our infrastructure, how to set-up hardware and software, how you can start your own community network, our community rules, and more.

+

This website is maintained by our volunteers, much like the rest of our services. This means you can help us improve it by adding missing information, clarifying confusing points, or even just fixing typos you notice while you’re reading. See Contribute to SCN Docs to learn more about how you can contribute to this website.

+

If you are looking for our main website, it is located at www.seattlecommunitynetwork.org.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/faq/what/index.html b/faq/what/index.html new file mode 100644 index 0000000..135a222 --- /dev/null +++ b/faq/what/index.html @@ -0,0 +1,191 @@ + + + + + + + + What is a Community Network? - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

What is a Community Network?

+

"Community Networks (CNs) are crowd-sourced collaborative networks, developed in a bottom-up fashion by groups of individuals – i.e. communities – that design, develop and manage the network infrastructure as a common resource. Importantly, at the centre of CNs and the socio-economic ecosystems they generate lay the communities and their members, who are essential to initiate, maintain and guarantee the success of these connectivity efforts."

+

Source: Building Community Network Policies: A Collaborative Governance towards Enabling Frameworks

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/faq/why/index.html b/faq/why/index.html new file mode 100644 index 0000000..5502ff5 --- /dev/null +++ b/faq/why/index.html @@ -0,0 +1,191 @@ + + + + + + + + Why have a Community Network? - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Why Have a Community Network?

+

Internet access is a foundational component of many aspects of modern life, nearly as important as electricity and water service. Though internet service is generally available in many areas of the United States, there are still areas that are underserved because of a variety of geographic and socio-economic factors, or simply because traditional internet service providers do not find it profitable to install and maintain the necessary infrastructure to serve some areas with adequate internet.

+

Community networks attempt to address this digital divide by connecting underserved communities. Because a community network is created, managed, and maintained by volunteers, it is able to serve areas that may not have other affordable internet options.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/img/favicon.ico b/img/favicon.ico new file mode 100644 index 0000000..e85006a Binary files /dev/null and b/img/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..d5fbc65 --- /dev/null +++ b/index.html @@ -0,0 +1,208 @@ + + + + + + + + My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Seattle Community Network Docs

+

Welcome to the documentation website for the Seattle Community Network! If you're looking for our main website, it is located at https://www.seattlecommunitynetwork.org.

+

You're in the Right Place

+

Seattle Community Network has a place for everyone. Whether you'd like to join +to get free Internet, get involved to help out your community, learn some skills +so that you can get a job, or all of the above!

+

PRO TIP We are a community. It's in our name! So, why not start by joining our community? It's easy.

+

What's here?

+

Some topics you can find on this website include:

+
    +
  • FAQ - get the answers to some common questions.
  • +
  • Community - get involved and learn more about our community, our rules, and what we're up to.
  • +
  • Learn - gain some new skills that you can use to help out with our networks.
  • +
  • Infrastructure - get the details on how our networks work behind the scenes.
  • +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + + Next » + + +
+ + + + + + + + + + + diff --git a/infrastructure/epc-setup/index.html b/infrastructure/epc-setup/index.html new file mode 100644 index 0000000..a7c0e38 --- /dev/null +++ b/infrastructure/epc-setup/index.html @@ -0,0 +1,191 @@ + + + + + + + + Step 1. LTE Core Network Setup - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Step 1: CoLTE/EPC (LTE Core Network) Setup

+

Our core networks use the CoLTE project maintained by the UW ICTD Lab.

+

For information on how to install and configure CoLTE, visit the tutorial we wrote with them!

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/infrastructure/hardware/index.html b/infrastructure/hardware/index.html new file mode 100644 index 0000000..9994671 --- /dev/null +++ b/infrastructure/hardware/index.html @@ -0,0 +1,231 @@ + + + + + + + + Hardware Overview - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Our Hardware

+

This page will be an overview of some of the core pieces of hardware that we use to deploy our sites.

+

This page is in development, please contact us at lcl@seattlecommunitynetwork.org if you would like to learn more about the hardware we use.

+

TODO

+

Network Site Equipment

+

Base Station (eNodeB)

+

Baicells Nova 233 Base Station Marketing Image

+

Baicells Nova 233 3.5GHz 1W Gen2

+

More info here

+

Panel Antennas (eNodeB)

+

Alpha Wireless Antenna Marketing Image

+

Alpha Wireless, 3.3-3.8GHz, 2x2 MIMO, 18dBi, +/-45°, 65°

+

More info here

+

Core Network Computer (EPC)

+

Qotom Mini PC Marketing Image

+

Qotom Mini PC Q190G4N S07

+

Key features: +- 4 ethernet ports +- designed to be run 24/7 +- small and quiet +- cheap

+

More info here

+

User Access Devices

+

LTE Consumer Premises Equipment (CPE)

+

Baicells Atom CPE Marketing Image

+

Baicells Atom OD04 3.5GHz 14dBi

+

More info here

+

Outdoor WiFi Router

+

Mikrotik OmniTIK 5 PoE ac Marketing Image

+

Mikrotik OmniTIK 5 PoE ac

+

Outdoor router of choice for NYC Mesh, so it has been tried and tested. Good balance of quality and price.

+

More info here

+

Home WiFi Router

+

TP-Link Archer A5 Router Marketing Image

+

TP-Link Archer A5 Router

+

More info here

+

CBRS-Compatible Unlocked Smartphone

+

We purchase refurbished Google Pixel 4 smartphones because they are affordable, provide all +necessary smartphone features, and are CBRS-compatible.

+

Note that purchasing CBRS-compatible phones can be a logistical challenge. We've experienced trouble purchasing +from vendors that send incorrect models of phones that don't support CBRS band and we had to go back and forth. +Test your phones before distributing them!

+

Here is one spot to purchase refurbished phones.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/infrastructure/librenms-manager-setup/index.html b/infrastructure/librenms-manager-setup/index.html new file mode 100644 index 0000000..5580ce6 --- /dev/null +++ b/infrastructure/librenms-manager-setup/index.html @@ -0,0 +1,332 @@ + + + + + + + + Network Monitoring 1. LibreNMS Network Manager Configuration - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

LibreNMS Network Manager Configuration

+

Seattle Community Networks uses SNMP to monitor network nodes. LibreNMS is used for Network Management, Dashboard generation and Alerting.

+

LibreNMS Manager Installation:

+

Install LibreNMS +Install and Configure LibreNMS on Ubuntu with nginx

+

Network-Specific Configuration:

+

Change active user to librenms: +sudo su - librenms

+

Edit /opt/librenms/config.php:

+
<?php
+
+$config['user'] = 'librenms';
+$config['base_url'] = "/";
+$config['snmp']['community'] = array('<SNMP COMMUNITY STRING>');
+$config['auth_mechanism'] = "mysql"; # default, other options: ldap, http-auth
+$config['nets'][] = "10.0.0.0/24"; # Replace with your Management Network Subdomain
+$config['rrd_purge'] = 0;
+$config['enable_billing'] = 1;
+$config['show_services'] = 1;
+
+

As user 'librenms', run /opt/librenms/snmp-scan.php, to scan the configured network for snmp hosts

+

Adding Baicells OS configuration to LibreNMS

+

As user 'librenms' on the librenms server, create the following files and update their contents accordingly: +* For OS detection, ~librenms/includes/definitions/rts.yaml:

+
    os: rts
+        text: 'Baicells RTS'
+        type: network
+        icon: rts
+        over:
+        - { graph: device_bits, text: 'Device Traffic' }
+        - { graph: device_processor, text: 'CPU Usage' }
+        - { graph: device_mempool, text: 'Memory Usage' }
+        discovery:
+        - sysDescr:
+            - 'CELL'
+
+
    +
  • For defining custom RTS OS sensors, ~librenms/includes/definitions/discovery/rts.yaml:
  • +
+
mib: BAICELLS-MIB
+modules:
+    os:
+        hardware: BAICELLS-MIB::hardwareVersion.0
+        serial: BAICELLS-MIB::sn.0
+        version: BAICELLS-MIB::softwareVersion.0
+    sensors:
+        count:
+            data:
+                -
+                    oid: ulThroughput
+                    num_oid: '.1.3.6.1.4.1.53058.190.7.{{ $index }}'
+                    descr: 'Upload Throughput'
+                    group: 'Throughput'
+                    index: 'ulthroughput.{{ $index }}'
+                -
+                    oid: dlThroughput
+                    num_oid: '.1.3.6.1.4.1.53058.190.8.{{ $index }}'
+                    descr: 'Download Throughput'
+                    group: 'Throughput'
+                    index: 'dlThroughput.{{ $index }}'
+                -
+                    oid: ulPrbUtilization
+                    num_oid: '.1.3.6.1.4.1.53058.190.9.{{ $index }}'
+                    descr: 'Upload PRB Utilization'
+                    group: 'Utilization'
+                    index: 'ulPrbUtilization{{ $index }}'
+                -
+                    oid: dlPrbUtilization
+                    num_oid: '.1.3.6.1.4.1.53058.190.10.{{ $index }}'
+                    descr: 'Download PRB Utilization'
+                    group: 'Utilization'
+                    index: 'dlPrbUtilization.{{ $index }}'
+        frequency:
+            data:
+                -
+                    oid: carrierBwMhz
+                    num_oid: '.1.3.6.1.4.1.53058.100.7.{{ $index }}'
+                    divisor: 5
+                    descr: 'Carrier Bandwidth'
+                    index: 'carrierBwMhz.{{ $index }}'
+        percent:
+            data:
+                -
+                    oid: eRABEstablishSuccessRate
+                    num_oid: '.1.3.6.1.4.1.53058.190.3.{{ $index }}'
+                    descr: 'ERAB Establishment Success Rate'
+                    group: 'LTE'
+                    index: 'eRABEstablishSuccessRate.{{ $index }}'
+                -
+                    oid: hoSuccInterEnbS1Rate
+                    num_oid: '.1.3.6.1.4.1.53058.190.4.{{ $index }}'
+                    descr: 'Inter MME S1 Handover Success Rate'
+                    group: 'LTE'
+                    index: 'heSuccInterEnbS1Rate.{{ $index }}'
+                -
+                    oid: hoSuccInterEnbRate
+                    num_oid: '.1.3.6.1.4.1.53058.190.5.{{ $index }}'
+                    descr: 'Inter MME Handover Success Rate'
+                    group: 'LTE'
+                    index: 'hoSuccInterEnbRate.{{ $index }}'
+                -
+                    oid: rrcBuildSuccessRate
+                    num_oid: '.1.3.6.1.4.1.53058.190.6.{{ $index }}'
+                    descr: 'RRC Build Success Rate'
+                    group: 'LTE'
+                    index: 'rrcBuildSuccessRate.{{ $index }}'
+
+
    +
  • For defining a custom OS class to use Wireless sensors, ~librenms/LibreNMS/OS/Rts.php (note: pay attention to capitalization)
  • +
+
<?php
+namespace LibreNMS\OS;
+
+use LibreNMS\Device\WirelessSensor;
+use LibreNMS\Interfaces\Discovery\Sensors\WirelessClientsDiscovery;
+use LibreNMS\Interfaces\Discovery\Sensors\WirelessUtilizationDiscovery;
+use LibreNMS\OS;
+
+class Rts extends OS implements WirelessClientsDiscovery
+{
+    public function discoverWirelessClients()
+    {
+        $oid = '.1.3.6.1.4.1.53058.100.11.0'; //BAICELLS-MIB::ueConnections.0
+        return array(
+            new WirelessSensor('clients', $this->getDeviceId(), $oid, 'rts', 1, 'UE Connections')
+        );
+    }
+}
+
+ + +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/infrastructure/librenms-setup/index.html b/infrastructure/librenms-setup/index.html new file mode 100644 index 0000000..b83718e --- /dev/null +++ b/infrastructure/librenms-setup/index.html @@ -0,0 +1,326 @@ + + + + + + + + Network Monitoring 2. LibreNMS Agent Configuration - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

LibreNMS Agent Configuration

+

Adding a New Node to LibreNMS

+

Both the eNodeB and the ePC must be configured individually in order for them to report statistics to the SNMP Manager. Since the eNodeB is not directly accessible from the management VPN, we configure an SNMP proxy on the ePC to pass SNMP statistics to the Management host.

+

ePC SNMP Configuration

+
    +
  • +

    Install snmpd to the ePC node: +$ sudo apt install snmpd

    +
  • +
  • +

    Modify /etc/snmp/snmpd.conf:

    +
  • +
+
sysLocation     <SITE NAME STRING>
+sysContact      lcl@seattlecommunitynetwork.org
+sysServices     72
+master          agentx
+agentAddress    udp:161
+com2sec readonly <SNMP Manager IP Address> <SNMP COMMUNITY STRING>
+com2sec -Cn ctx_baicells readonly <SNMP Manager IP Address> enodeb
+group readonlygroup v2c readonly
+view all included .1
+access readonlygroup "" v2c noauth exact all none none
+access readonlygroup ctx_baicells v2c noauth prefix all none none
+proxy -Cn ctx_baicells -v 2c -c private 192.168.151.1 .1.3
+
+

This configuration allows us to access SNMP data on the EPC with the standard community string (refer to internal standards documentation). but will proxy the Baicells SNMP data when we send the community string ‘enodeb’

+
    +
  • Update the snmpd service file to automatically restart snmpd on crash:
  • +
  • Edit /lib/systemd/system/snmpd.service, modify the 'ExecStart' line, and add the 'ExecReload', 'Restart', and 'RestartSec' lines:
  • +
+
[Unit]
+Description=Simple Network Management Protocol (SNMP) Daemon.
+After=network.target
+ConditionPathExists=/etc/snmp/snmpd.conf
+
+[Service]
+Type=simple
+ExecStartPre=/bin/mkdir -p /var/run/agentx
+ExecStart=/usr/sbin/snmpd -LO2w -u Debian-snmp -g Debian-snmp -I -smux,mteTrigger,mteTriggerConf -f -p /run/snmpd.pid
+ExecReload=/bin/kill -HUP $MAINPID
+Restart=on-failure
+RestartSec=5s
+
+[Install]
+WantedBy=multi-user.target
+
+
    +
  • Enable and restart snmpd:
  • +
+
Sudo systemctl daemon-reload
+sudo systemctl enable snmpd
+sudo systemctl restart snmpd
+
+

Baicells SNMP configuration

+
    +
  • +

    Log into the Baicells configuration console: +https://<Baicells IP Address>

    +
  • +
  • +

    From the left menu, select System

    +
  • +
  • +

    Select SNMP +Example Screenshot: enabling SNMP in the Baicells Console

    +
  • +
  • Under ‘SNMP Switch,’ select ‘Enable’
  • +
  • Configure the following options:
      +
    • Community String: private
    • +
    • Contact: lcl@seattlecommunitynetwork.org
    • +
    • Location: \<SITE NAME STRING> (String should not have any spaces)
    • +
    • Source: Any
    • +
    +
  • +
+

Adding the Node to LibreNMS

+
    +
  • +

    If the ePC is running, librenms should be able to auto-discover it. Run this command from a shell on the management host: +sudo -u librenms lnms scan

    +
  • +
  • +

    LibreNMS should print a status message that it was able to add a new device.

    +
  • +
  • +

    When first discovered, the ePC will show up generically as it’s ip address. Edit the hostname, but clicking ‘Edit Device’ (gear icon):

    +
  • +
  • Click the red pencil icon, and change the ip address to the hostname
  • +
  • +

    Fill ‘Overwrite IP’ with the ePC IP address

    +
      +
    • Note: If the IP is not changed to the hostname, you will not be able to add the eNodeB by it’s IP address +Example Screenshot: Updating ePC Hostname in LibreNMS
    • +
    +
  • +
  • +

    The Baicells eNB needs to be added manually: From LibreNMS, select Devices and click “Add Device” +Example Screenshot: Manually adding eNodeB to LibreNMS

    +
  • +
  • +

    Add a new device, with the following configurations:

    +
  • +
  • Hostname:
  • +
  • Community: ‘enodeb’
  • +
  • +

    Force Add: On

    +
  • +
  • +

    Note: If you receive an error message stating that a device with the specified IP already exists, make sure that you have successfully changed the eNodeB’s hostname per the previous step.

    +
  • +
  • +

    Once the device is added, click the ‘Edit Device’ icon (gear icon) and update the following values:

    +
  • +
  • Display name: \<eNB Cell Name>
  • +
  • Overwrite device contact: lcl@seattlecommunitynetwork.org
  • +
+

Other helpful notes:

+
    +
  • +

    Baicells eNB config guide

    +
  • +
  • +

    How to SSH into Baicells eNB:

    +
  • +
  • SSH using port 27149 (username same as normal web-based login)
  • +
  • Convert the MAC address of this eNB to link local address: http://www.sput.nl/internet/ipv6/ll-mac.html
  • +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/infrastructure/peering/index.html b/infrastructure/peering/index.html new file mode 100644 index 0000000..fe95ffd --- /dev/null +++ b/infrastructure/peering/index.html @@ -0,0 +1,261 @@ + + + + + + + + Public ASN Peering - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Public ASN Peering

+

Local Connectivity Lab operates AS54429

+

Our peering Policy is Yes

+

Please contact us to peer with our network.

+

Note this network is our public ASN, not the Seattle Community Network itself. If you would like to join the network visit our connect page.

+

Seattle Community Network (SCN) is a community network dedicated to providing fair access to underserved communities all across the Puget Sound. Learn more on our FAQ.

+

Peering Policy

+
    +
  • Local Connectivity Lab has an open peering policy.
  • +
  • We have no requirements in terms of traffic, size, support/SLA, etc.
  • +
  • We operate both IPv4 and IPv6. Peering via both protocols is appreciated.
  • +
+

Locations

+ + + + + + + + + + + + + + + +
BuildingAddressPorts
Westin2001 6th Avenue, Seattle, WA1G / 10G
+

Exchanges

+ + + + + + + + + + + + + + + + + + + + + + + +
ExchangeCityIPv4IPv6ASNsRoutesSpeed
Seattle Internet Exchange (SIX)Seattle, WA206.81.81.1502001:504:16::d49d336~192K10G
+

Peering Data

+

ASN: 54429
+Peering Contact: tech@seattlecommunitynetwork.org
+PeerDB Page: https://as54429.peeringdb.com
+As we are a non-profit, please consider providing as many routes as possible, including upstream or other routes.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/infrastructure/proxmox-vaultwarden-deployment/index.html b/infrastructure/proxmox-vaultwarden-deployment/index.html new file mode 100644 index 0000000..6fc4c3a --- /dev/null +++ b/infrastructure/proxmox-vaultwarden-deployment/index.html @@ -0,0 +1,402 @@ + + + + + + + + Proxmox Deployment Guide - Vaultwarden - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Deployed by: Esther Jang, Paul Phillion, Rudra Singh

+
+

Section 1: What is Proxmox?

+
    +
  • Proxmox VE (Virtual Environment) is an open-source server management platform designed to deploy and manage virtual machines (VMs) and containers.
  • +
  • It integrates KVM hypervisor and LXC containers, enabling users to manage virtual infrastructure through a web-based interface.
  • +
+

Section 2: Access Requirements for Proxmox VE at Seattle Community Network

+
    +
  • To submit a request for a SCN self-hosted Proxmox VM on our private cloud, please fill out this form.
  • +
  • If you are working on a project for SCN and need access to the Proxmox VE, please continue reading.
  • +
  • Next, you will need access to the OpenVPN. Proxmox VE can only be accessed on the VPN. Specific details can be obtained from the SCN discord. Once connected to the VPN, a specific IP will be provided for you to access the Proxmox VE, where you can input your credentials.
  • +
+

Section 3: Setting up your VM

+
    +
  • +

    Install SSH

    +
  • +
  • +

    Install Keys

    +
  • +
  • +

    Test SSH CLI

    +
  • +
+

Section 4: SSH Troubleshooting

+
    +
  • Please let the SCN discord know if you have trouble SSH'ing in. A useful command during setup was sudo ufw status. If it is active, use ufw disable. The expected status should be inactive. If that still doesn’t work, try restarting the VM from the Proxmox VE.
  • +
+

Section 5: Beginning Deployment: Vaultwarden

+
docker pull vaultwarden/server:latest
+docker run -d --name vaultwarden -v /vw-data/:/data/ --restart unless-stopped -p 80:80 vaultwarden/server:latest
+
+

Section 6: Docker Compose

+
    +
  • Create a Docker Compose file:
  • +
+
version: "3"
+
+services:
+  vaultwarden:
+    container_name: vaultwarden
+    hostname: vaultwarden9
+    ports:
+      - "127.0.0.1:8080:80"
+    environment:
+      - LOG_FILE=/log/access.log
+      - LOG_LEVEL=info
+      - EXTENDED_LOGGING=true
+    image: vaultwarden/server:latest
+    restart: unless-stopped
+    volumes:
+      - /opt/vw-data:/data
+      - /var/log/vw:/log
+
+
    +
  • To start and run your container application in detached mode, use:
  • +
+
docker-compose up -d
+docker-compose start
+docker-compose stop
+docker-compose restart
+
+

Section 7: Azure DNS

+
    +
  • For this deployment, we're utilizing a public IP address. This address must be configured within Azure DNS to point to our chosen domain name.
  • +
  • Navigate to Home - Microsoft Azure with your account.
  • +
  • Open “DNS zones”. If you were using a private IP, you would use “Private DNS zones”.
  • +
  • We will be creating a subdomain under seattlecommunitynetwork.org.
  • +
  • Click on that, and under DNS management, click recordsets, and click add.
  • +
  • Insert the beginning of the subdomain name, which in our case is “vaultwarden”.
  • +
  • Below, under IP address, insert your IP, and create your new subdomain.
  • +
+

Section 8: Enabling Nginx

+
sudo apt update
+sudo apt install nginx
+sudo systemctl enable nginx
+sudo systemctl start nginx
+
+
    +
  • Next, go to /etc/nginx/sites-enabled/default. Delete everything inside default and paste this:
  • +
+
# The `upstream` directives ensure that you have a http/1.1 connection
+# This enables the keepalive option and better performance
+#
+# Define the server IP and ports here.
+upstream vaultwarden-default {
+  zone vaultwarden-default 64k;
+  server 127.0.0.1:8080;
+  keepalive 2;
+}
+
+# Needed to support websocket connections
+# See: https://nginx.org/en/docs/http/websocket.html
+# Instead of "close" as stated in the above link we send an empty value.
+# Else all keepalive connections will not work.
+map $http_upgrade $connection_upgrade {
+    default upgrade;
+    ''      "";
+}
+
+# Redirect HTTP to HTTPS
+server {
+    listen 80;
+    listen [::]:80;
+    server_name vaultwarden.seattlecommunitynetwork.org;
+
+    if ($host = vaultwarden.seattlecommunitynetwork.org) {
+        return 301 https://$host$request_uri;
+    }
+    return 404;
+}
+
+server {
+    # For older versions of nginx appened http2 to the listen line after ssl and remove `http2 on`
+    listen 443 ssl;
+    listen [::]:443 ssl;
+   # http2 on;
+    server_name vaultwarden.seattlecommunitynetwork.org;
+
+    # Specify SSL Config when needed
+    ssl_certificate /etc/path...;
+    ssl_certificate_key /etc/path...;
+    ssl_trusted_certificate /etc/path...;
+
+    client_max_body_size 525M;
+
+    location / {
+      proxy_http_version 1.1;
+      proxy_set_header Upgrade $http_upgrade;
+      proxy_set_header Connection $connection_upgrade;
+
+      proxy_set_header Host $host;
+      proxy_set_header X-Real-IP $remote_addr;
+      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+      proxy_set_header X-Forwarded-Proto $scheme;
+
+      proxy_pass http://vaultwarden-default;
+    }
+
+    # Optionally add extra authentication besides the ADMIN_TOKEN
+    # Remove the comments below `#` and create the htpasswd_file to have it active
+    #
+    #location /admin {
+    # See: https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/
+      #auth_basic "Administrator's Area";
+      #3auth_basic_user_file /path/to/htpasswd_file;
+
+      #proxy_http_version 1.1;
+      #proxy_set_header Upgrade $http_upgrade;
+      #proxy_set_header Connection $connection_upgrade;
+
+      #proxy_set_header Host $host;
+      #proxy_set_header X-Real-IP $remote_addr;
+      #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+      #proxy_set_header X-Forwarded-Proto $scheme;
+      #proxy_pass http://vaultwarden-default;
+    }
+}
+
+

Section 9: Obtain SSL Certificates

+
sudo apt install certbot python3-certbot-nginx
+sudo certbot --nginx -d vaultwarden.seattlecommunitynetwork.org
+
+
    +
  • Certbot will modify your Nginx configuration to handle HTTPS and redirect from HTTP to HTTPS.
  • +
  • Ensure that you copy the file paths provided at the conclusion of the certbot process into the default nginx configuration file, replacing the corresponding comments with these paths.
  • +
+

Section 10: Ensure Everything is Running

+
sudo systemctl status nginx
+
+

Section 11: Additional Features

+
    +
  • Enabling admin panel:
  • +
+

Go back to /etc/nginx/sites-enabled/default and uncomment the admin section at the bottom. Follow directions at Nginx Admin Guide to encrypt your admin password as a .env file (preferably using argon CLI).

+

Once done, make sure you create a .env file in the directory where the compose file is with VAULTWARDEN_ADMIN_TOKEN=[insert your hashed admin token].

+

Then in your compose, add these two lines under environment:

+
- ADMIN_TOKEN=${VAULTWARDEN_ADMIN_TOKEN}
+- DOMAIN=https://vaultwarden.seattlecommunitynetwork.org
+
+

Restart the container and try logging into https://vaultwarden.seattlecommunitynetwork.org/admin.

+

Once logged in, SMTP and 2FA enabling settings can be configured on the home page.

+

Section 12: Data Backup

+
    +
  • Enabling backups for Vaultwarden is a simple process. Please review the attached backup bash script, which facilitates the transfer of Vaultwarden data to another virtual machine. Additionally, there is a cleanup bash script designed to retain only the most recent file in the other VM, deleting all others. Feel free to modify the scripts as necessary to suit your specific requirements.
  • +
+

Backup script:

+
#!/bin/bash
+
+docker-compose down
+datestamp=$(date +%m-%d-%Y)
+zip -9 -r /home/scn/backups/${datestamp}.zip /opt/vw-data*
+scp -i ~/.ssh-comm/id_rsa /home/scn/backups/${datestamp}.zip azureuser@[IP address]:~/backups/
+docker-compose up -d
+
+

Cleanup Script:

+
#!/bin/bash
+
+# Define the directory containing backup files
+backup_dir=~/backups
+
+# Go to the backup directory
+cd "$backup_dir" || exit
+
+# Find and delete older backup files (excluding the latest day)
+find . -type f -name '*.zip' ! -mtime -1 -exec rm {} +
+
+# Exit
+exit 0
+
+

Section 13: Using the Backup

+

To restore a data backup to the original virtual machine, simply unzip the file and delete the existing contents of /opt/vw-data. Then, transfer the contents of your zip file into this directory. Perform a quick restart of the container, and you will have successfully restored the version of the backup you selected.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/infrastructure/sas-setup/index.html b/infrastructure/sas-setup/index.html new file mode 100644 index 0000000..ed76a8e --- /dev/null +++ b/infrastructure/sas-setup/index.html @@ -0,0 +1,522 @@ + + + + + + + + Step 2. eNodeB and SAS Setup - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Step 2: eNodeB and SAS Setup

+

Introduction

+

Despite CBRS being a relatively open frequency band, the processes for spectrum access are still somewhat opaque and require significant capital investment and/or ISP-level resources to set up. To clarify this process, here’s a step by step walkthrough tutorial of the setup of a Baicells eNodeB (eNB) base station running in the Citizen’s Broadband Radio Service (CBRS) spectrum band (or band 48).

+

Before following this tutorial, you should have completed the setup of a LTE Evolved Packet Core (EPC) to control your eNB, for which the setup of an open source version based on open5gs is outlined in this tutorial.

+

I. Get set up with a Spectrum Access System (SAS)

+

A. Why get set up with a SAS?

+

Current FCC regulations require all CBRS equipment (called a CBSD) to be registered on a Spectrum Access System (SAS) that coordinates all spectrum assignments and ensures that no transmissions interfere with each other. This will likely require a commercial agreement with a SAS provider such as Google, Federated Wireless, etc. This tutorial uses the Google SAS.

+

B. CPI License

+

At least one member of your team will require “Certified Professional Installer” (CPI) training and license in order to hold legal responsibility for and sign off on device installations. Most SAS providers will offer training at about $500 for both an online training course and the certification exam. If you aren’t able to get someone on your team certified, be sure to collaborate with a CPI! Feel free to contact us at the Local Connectivity Lab if you need support for your community project in this regard, and we can figure out what is feasible.

+

The following are some links and helpful notes about this process: +* https://wifidevan.wordpress.com/cbrs-certified-professional-installer-cpi-study-notes/ +* https://alliancecorporation.ca/webinars/webinars-webinars/cbrs-for-beginners-part-2-by-commscope/ +* https://cbrs.wirelessinnovation.org/acronyms

+

C. SAS Pricing Agreements

+

For Google, the price options provided us in summer 2020 were:

+
    +
  • Fixed Wireless
      +
    • SAS services are billed per link/household so you pay for each CPE (Customer Premises Equipment) CBSD registered with SAS.
    • +
    • CBSDs that operate as base stations are free of charge. Price Per Customer Link $2.25/month.
    • +
    +
  • +
  • Mobility/Private LTE (price is based on CBSD categoris)
      +
    • Category A CBSD
        +
      • max transmit capability: 30 dBm/10 MHz = 20 dBm/MHz or “1 Watt”
      • +
      • mounted under 6m Height Above Average Terrain measured 3-16 km away from site
      • +
      • $2.67/month
      • +
      +
    • +
    • Category B CBSD
        +
      • max transmit capability: Maximum EIRP of 47 dBm/10 MHz = 37 dBm/MHz or “50 Watt"
      • +
      • $13.33/month.
      • +
      +
    • +
    +
  • +
+

D. SAS Registration

+

CBSDs must register their transmit capabilities with the SAS using either the “one-step” or “multi-step” process.

+

The one-step process requires you to input all installation parameters and sign them with the CPI certificate all on the base station itself, or via a cloud domain proxy such as used by Baicells. Not all base stations support this and the interfaces for doing so might vary widely, so “multi-step” is typically recommended.

+

II. Register device in SAS portal

+

This tutorial will be walking through steps following the specifics of the Google SAS portal interface, but the steps should be generalizable to other SAS portals.

+

A. Once you have an account on an SAS service, register your devices on their portal or dashboard.

+

The Google SAS portal can be found at: https://wirelessconnectivity.google.com/sas/

+

B. Our Setup

+

Our test setup in the lab includes:

+
    +
  • 1W Baicells Nova 233 base station in the CBRS band mounted on the 6th floor balcony of our UW computer science building.
  • +
  • Alpha Wireless 18 dBi-gain panel antenna with a beamwidth of 65 degrees (model AW3014-T4), mounted straight ahead and not tilted down.
  • +
+

C. Example Configuration

+

An example configuration for this setup is shown below.

+

Google SAS Configuration Screen

+

The configuration screen is a right-hand sidebar next to the map view, hence the unwieldy aspect ratio.

+

Explanation of parameters:

+
    +
  1. CBSD Category (A or B):
      +
    • Defined by rules in Section I.C above
    • +
    +
  2. +
  3. User ID
      +
    • Specified by the SAS provider when you register
    • +
    +
  4. +
  5. +

    FCC ID and Serial Number:

    +
      +
    • Both the radio and antenna model must be pre-authorized for use with CBRS by the FCC.
    • +
    • The FCC ID is used to identify this approved device type.
    • +
    • The serial number specifies the exact device identity.
    • +
    • Both can usually be found on the outside of the device (circled in image below). +
    • +
    +
  6. +
  7. +

    Beamforming Gain, Beamwidth

    +
      +
    • Based on antenna specs in II.B
    • +
    +
  8. +
  9. EIRP
      +
    • Effective Isotropic Radiated Power of your system including both the base station radio and antenna.
    • +
    • For a Cat B CBSD, this must be 46 dBm/10 MHz=36 dBm/MHz or lower.
    • +
    • Calculate this value by adding the max transmit power (actually power density per MHz) of the base station, in our case 28 dBm, to the antenna beamforming gain, in our case 18 dBi; 28+18=36 dBm/MHz.
    • +
    • For the units requested by the Google interface, add 10 to this value to specify power per 10 MHz instead of per MHz.
    • +
    +
  10. +
  11. Height
      +
    • Specified in terms of height Above Ground Level (AGL) which you can measure using a rangefinder/ measuring tape/ building plan, or in height Above Mean Sea Level (AMSL).
    • +
    • Not in terms of HAAT as in the Cat A/B definition.
    • +
    • Must be accurate to within 3 m.
    • +
    +
  12. +
  13. +

    Azimuth

    +
      +
    • Refers to the compass heading/ direction that the antenna is pointing (set this to 0 for an omnidirectional antenna).
    • +
    • This FCC tool is extremely helpful for calculating the azimuth based on the antenna’s gps location and that of a structure you are pointing it at.
    • +
    • You can get these GPS coordinates via Google Maps or Google Earth. +
    • +
    +
  14. +
  15. +

    Air Interface

    +
      +
    • E_UTRA is the LTE radio standard used by our Baicells box.
    • +
    • The only “supported spec” currently available for Baicells is FFS (according to a forum post, linked here).
    • +
    +
  16. +
  17. Location:
      +
    • In the Google interface, set the site location in GPS coordinates under the tab labeled with the map pin icon. (not shown)
    • +
    +
  18. +
  19. Parameters under "CBSD Info"
      +
    • Call Sign
        +
      • As far as I can tell, this can be any reasonable alphanumeric string as long as it is unique and matches the value of the “call sign” parameter as sent over by the eNB or domain proxy.
      • +
      • You will set this in the SAS interface as well as either the eNB or Baicells Cloud Core (they all need to match).
      • +
      +
    • +
    • Others
        +
      • These should match the settings with the same name on the eNB’s local management portal, shown on the “Basic Info” page in section IV.A below.
      • +
      +
    • +
    +
  20. +
+

D. CPI Signature

+

When the parameters are all filled out, click the big red “Ready for CPI” button at the bottom of the panel (not shown here). On the CPI’s version of the interface, it will provide a place to “sign” the configuration with their CPI certificate, which they will upload to the interface. This must happen before the device can get a spectrum grant.

+

E. Status Tab

+

After the CPI signs the eNB configuration, under the “Status” tab (visible in the config panel), you should see “Not yet Registered” (or a similar message) because the eNB has not checked in to the Google SAS yet with its matching parameters to complete the multi-step process. If something has otherwise gone wrong, you’ll see an error message here.

+ + +

III. Steps in Baicells Cloud interface

+

A. Make a Baicells OMC account.

+

Due to Baicells’ use of a “domain proxy” for their SAS requests, you will need to make a new user account in the Baicells Operators Management Console (OMC): https://cloudcore.baicells.com:4443/

+

This is distinct from their paid “Cloud Core” service which we will not be using in this tutorial, although the management portal is the same.

+

B. Take note of the CloudKey

+

Once you have made an account, note the 6-letter “CloudKey” in the upper right corner of the screen (circled in red).

+

+

This will need to be inputted into the local eNB management portal for the eNB to check into the Cloud OMC.

+

On your version of this portal, if you’re doing this for the first time, you shouldn’t see any eNBs already present.

+

C. Set your SAS service provider.

+

Navigate to Advance→SAS in the left hand menu, and then click the gear icon on the upper right corner, which has the hover text “Settings.”

+

IV. Steps in Baicells management interface

+

A. Local Management Portal

+

The Baicells eNodeB (eNB) is best managed through the browser-based management portal; the current command line interface is accessible but extremely limited.

+

The default IP address of the management portal (and that of most Baicells equipment I’ve seen) is 192.168.150.1, and the default login credentials are admin/admin. I would recommend changing the admin login credentials to be more secure.

+

Connect your computer to the eNB via Ethernet, and navigate to this IP address in your browser (using http://192.168.150.1, not https).

+

Baicells Initial Login Screen:

+

+

BTS Info→“Basic Info” Page visible upon login:

+

+

B. Upgrade firmware

+

Upgrade the firmware to the latest firmware version that supports SAS functionality, or verify that it is already up to date.

+

You can check the official firmware page under the correct eNB model. The Nova 233 CBRS small cell we’re using is model mBS1105. The latest firmware version after which SAS is officially supported is BaiBS_RTS_3.6.6.IMG (as of Feb 2021), for which the direct download is available here.

+

+

Do not skip this step, otherwise none of the following steps will work right.

+

C. Get everything connected

+

Once the firmware is upgraded, you will want to get the eNB connected to your local LTE core network (EPC) as well as to the Internet so it can contact the necessary SAS infrastructure.

+

1. Configure Internet Access (WAN)

+

Navigate to the Network→WAN/LAN/VLAN tab on the left hand menu.

+

We will set the WAN interface IP address to 192.168.151.1, since the Baicells console requires (for whatever reason) a different subnet for the WAN as opposed to the LAN.

+

Then we will connect the eNB to an Ethernet port on the EPC that has the IP address 192.168.151.2 (as set up in our previous tutorial), which will act as the eNB’s Internet gateway.

+

Don’t forget to hit “Save” after each change you make in this interface. +

+

2. Check Internet access

+

At this point, if the EPC is configured correctly to pass eNB traffic to the Internet, the eNB should be able to ping an arbitrary IP address.

+

To test this, navigate to the Network→Diagnostics tab on the left hand menu and select “Ping” under the “Method of Diagnostics” dropdown menu. Set the “Target IP Domain” to be a highly reachable IP address on the Internet such as 1.1.1.1, which is the CloudFlare DNS server. Press “Implement.”

+

If the result is “Fail!” as in the screenshot, there is likely something wrong with your eNB’s Internet connection through the EPC; you should fix this issue before continuing. +

+

3. Reboot as needed

+

If a message appears that the eNB needs a reboot after the new settings are saved, navigate to the Reboot tab in the left hand menu and perform the reboot (Warm Reset is fine).

+

4. Attach to Baicells OMC

+

To configure the eNB to talk to the OMC as discussed in the prior section, navigate to the BTS Setting→Management Server tab in the management console and enter the CloudKey.

+

Within a few minutes, the eNB should appear in your Baicells Cloud OMC console, and the “Basic Info” page should show that the OMC is “Connected.” + +

+

5. Disable IPsec

+

For our purposes we will not be using IPsec between our EPC and eNB; the default IPSec configured is used for the Baicells Cloud EPC which we are not using.

+

Navigate to the Network→“MME&IPSec Binding” menu tab and set “IPSec Status” to “Disable.” You may also delete the IPSec tunnels as shown below. +

+

6. Disable GPS Sync when testing indoors.

+

Navigate to the “BTS Setting”→“Sync Setting” menu and disable both “Forced Sync” and “GPS Sync Switch,” in case you need to work with the base station in a location where you don’t have a strong GPS signal.

+

Some base stations will not start up normally or attach to the EPC unless they get a GPS signal, and we should avoid this behavior. +

+

7. Change the MME settings

+

Change the MME settings. Since we are using our local EPC, we will need to change the MME settings to reflect our MME’s IP address, on which it is listening for eNBs to attach, as well as other configurations. Navigate to the BTS Info→Quick Setting tab on the left hand menu.

+

+
    +
  • Disable RF
      +
    • You should set the “RF Status” setting to “Disable” before you change the MME IP, because attaching to the MME will normally cause the eNB’s radio to turn on.
    • +
    • Since we have not enabled the eNB to ask for spectrum coordinated by the SAS yet, turning on the radio may cause unwanted interference on someone else’s network.
    • +
    +
  • +
  • PLMN setting
      +
    • Remove the existing “PLMN ID” (by clicking the trash can symbol) and set it to the value that you have configured in your EPC.
    • +
    • In our networks, we use “91054” as our PLMN, so add this as a “Primary” and “NotReserved” PLMN by entering the number in the text box and clicking the “+” button.
    • +
    +
  • +
  • MME IP address
      +
    • Remove the existing MME IP associated with the old PLMN.
    • +
    • Add the new MME IP address, in our case 192.168.150.2, by entering it in the text box and clicking “+”. This MME IP should be associated with the newly added PLMN by default.
    • +
    • Save the changes and reboot the eNB (Warm Reset); after the reboot has finished (within a few minutes), the eNB should attach to the MME.
    • +
    • If you navigate to BTS Info→Basic Info, you should see the MME Status change from “Not Connected” to “Connected.” If you are looking at the MME logs on the EPC, you will also see the record that an eNB has attached.
    • +
    +
  • +
+

8. Enable SAS

+

SAS should only be enabled after successfully attaching the eNB to the MME. Unfortunately, when SAS is enabled, the eNB will not attach to the MME unless it has a currently valid authorization to transmit on a certain frequency. However, until it is attached to an MME, the Baicells Cloud OMC will not provide it this authorization.

+

So we need to have SAS disabled first with the RF also disabled, attach the eNB to the MME, and then enable SAS.

+

Choose “Multi-step” under “SAS Registration Type,” as specified in Section I.E. Also choose “B” under “category,” and write in the other parameters to match the ones with the same name in the Google SAS configuration.

+

+

After you click “Save,” SAS should be enabled immediately. You should see the SAS enabled status change in the Baicells Cloud OMC. If all goes smoothly, your device should get an authorization to transmit within a few minutes and the radio should turn on!

+

9. Check Baicells CLOUD OMC to debug issues

+

You can check the status of the SAS authorization process in the Cloud OMC. Here you can find logs (upper right corner of SAS screen, shown in the screenshot below) with any error messages that may have occurred in the process. +

+
    +
  • Errors can be caused by invalid or non-matching parameter values, lack of CPI signature, lack of spectrum availability, etc.
  • +
  • In more difficult cases, after device registration the SAS may not respond to spectrum inquiries without sending any clear error messages. I have encountered this scenario when requesting spectrum around midnight, which may have been caused by brief database unavailability during the daily “SAS Sync” or IAP. My recommendation is to avoid requesting a new spectrum grant after 11 pm PST.
  • +
  • If you change anything about the equipment used on site or the location/orientation of the equipment, you need to change the SAS registration, have it re-signed by the CPI, and use the Baicells OMC to re-request a new spectrum authorization- this process is described in the following section.
  • +
+

V. How to change location, antenna properties, etc. after deployment

+

As an example, this section will show how you would change the equipment’s location upon moving from test site to deployment site.

+
    +
  1. Get the new GPS location either manually using Google Maps/Earth, or automatically using Baicells OMC’s GPS reading for the eNB if available.
  2. +
  3. +

    Google SAS steps

    +
      +
    • In the upper right hand corner of the Google SAS configuration for the deployed equipment (long narrow right side panel for a particular site), press the unlock button (shaped like a padlock) to make the configuration editable. +
    • +
    +

    +

    +
      +
    • To edit the site location, click on the map pin icon in the upper left corner of this same right hand configuration panel to enter the location panel. Enter the new GPS coordinates in the box. After your changes, lock the site configuration again. (If the red “Ready for CPI” button appears again at the bottom of the main configuration panel, go ahead and click it to prompt the CPI to sign.)
    • +
    +

    +

    +

    +
      +
    • You may have to wait a few minutes or hours for the changes to sync to the CPI’s SAS database view. If after a while the CPI still cannot see the location change, ask them to enter the new GPS coordinates in their own interface and re-sign the configuration.
    • +
    • Baicells Cloud OMC steps
    • +
    • +

      On the Baicells OMC, navigate to the Advance→SAS screen where you can see the list of CBRS devices and their SAS status. Click on the 3 dots ( ⠇) symbol before the serial number for a particular device and click on “Procedure” to enter the SAS procedure screen. +

      +
    • +
    • +

      On the Procedure screen, you can see the most recent SAS logs, relinquish and re-request active spectrum authorizations, or de-register and re-register devices. First click on the “Authorized” icon and click on the “Relinquishment req” button to relinquish the current spectrum authorization. Then the latter two icons will become greyed out, but the device will remain registered. +

      +
    • +
    • +

      We will need to fully de-register and re-register the device with the new parameters. Click the “Registered” icon and then the “De-register” button when it appears to de-register the device. +

      +
    • +
    • +

      Once the device is in the “Unregistered” state, click the “Unregistered” icon and then click the “Register req” button when it appears. If all goes well, the device should re-register, and also request and receive a new grant (completing the full procedure) within a few moments.

      +
    • +
    +
  4. +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/infrastructure/software/index.html b/infrastructure/software/index.html new file mode 100644 index 0000000..7a75c81 --- /dev/null +++ b/infrastructure/software/index.html @@ -0,0 +1,257 @@ + + + + + + + + Software Overview - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Our Software

+

Here is a list of the software that we use to deploy, maintain, and plan our network sites.

+

Networking

+

Local Services

+

We use the CoLTE project maintained by the University of Washington ICTD Lab +to provide services such as network monitoring, web-based administration, and local web and DNS serving/caching.

+

Evolved Packet Core (EPC)

+

Our EPC is powered by Open5GS, an open-source project for 4G and 5G core networks. Currently all of our networks are 4G networks.

+

Spectrum Access System (SAS)

+

We have a partnership with Google SAS to gain access to CBRS spectrum.

+

Learn more about our SAS setup here.

+

Network Monitoring and Alerting

+

We use LibreNMS and SNMPd to monitor our nodes and provide alerting. Our Baicells-specific Network Manager setup is documented here, and our instructions for configuring a new node can be found here.

+

Field Measurement

+

Network Performance Measurement Tool

+

The LCL Network Performance Measurement Tool is an Android App in development that will measure a variety of network metrics, including but not limited to ping, upload/download speed, signal strength. We will use this tool to easily capture and upload network metrics +in the field so that we can provide better estimates of what kind of Internet access that our users can expect to receive.

+

Network Cell Info Lite

+

Network Cell Info Lite is an Android App on the Google Playstore that is free to use (with advertisements) +and is capable of taking network metric measurements and recording them to upload. This is an option that we +use but are not satisfied with for many reasons, which is why we are developing our own app.

+

Site Planning

+

Google Earth

+

We primarily use the Google Earth Pro desktop application to do a rough line-of-sight evaluation. We perform what is called a "viewshed analysis" that allows us to determine what is visible from a specific point on Earth (e.g. a rooftop).

+

Ubiquiti Line of Sight

+

A web-based line of sight tool provided by Ubiquiti that contains helpful altitude data and diagrams. +A drawback is that it is specialized to provide data for Ubiquiti devices only.

+

Other resources

+

Facebook ISP Toolbox Line of Sight

+

A web-based line of sight tool provided by Facebook Connectivity that utilizes public LiDAR data. Unfortunately LiDAR data for the Seattle area is not present yet, although there is data for some areas in Tacoma.

+

Facebook ISP Toolbox Market Evaluator

+

A web-based market evaluator provided by Facebook Connectivity that can be used +to provide more context about the areas around potential network sites. It offers information about other service providers in the area, average household income, median speeds, and current lowest broadband price available.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/js/html5shiv.min.js b/js/html5shiv.min.js new file mode 100644 index 0000000..1a01c94 --- /dev/null +++ b/js/html5shiv.min.js @@ -0,0 +1,4 @@ +/** +* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed +*/ +!function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); diff --git a/js/jquery-3.6.0.min.js b/js/jquery-3.6.0.min.js new file mode 100644 index 0000000..c4c6022 --- /dev/null +++ b/js/jquery-3.6.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t + + + + + + + Crimping Ethernet Cables - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Crimping Ethernet Cables

+

In this article, you'll all about crimping ethernet cables!

+

What is crimping an ethernet cable?

+

Crimping an ethernet cable is the process of attaching connectors onto the ends of ethernet cables. This process is also called 'RJ45 crimping' because RJ45 is the name of the connectors that are used for ethernet cables, and they are what is being crimped.

+

Why?

+

Setting up networks involves setting up long ethernet cable connections between different devices. Instead of buying premade ethernet cables of varying lengths (e.g. 5ft, 10ft, 50ft, etc.), it's more practical to just have a big spool of cabling that we can roll out and cut to the exact length we need.

+

Therefore we need to be able to attach RJ45 connectors to the ends of these cut cables so that we can actually plug them in!

+

Crimping Kit

+

Here are some tools you should have in your crimping kit!

+

RJ45 Crimping Tool

+

RJ45 Crimping Tool

+

An RJ45 crimping tool is the most essential tool. Although it's technically possible to crimp ethernet cables without this specialized tool, it's not very practical for crimping lots of cables.

+

Its primary utility is to do the actual 'crimping' part of compressing/crimping the tiny gold pins in the RJ45 connector onto the ethernet cables. It also has blades that can be used to cut or strip wires.

+

Cable Stripper

+

Cable Stripper

+

Cable strippers are used to take off the protecting shielding around cables and expose the inner wires. You can also do the same thing with a simple blade or pair of scissors. The trickiest part about stripping cables is trying to avoid cutting the inner wires!

+

RJ45 Connectors

+

RJ45 Connector

+

RJ45 connectors are required for crimping because they feature the 8 golden pins that get crimped onto the 8 wires of the ethernet cable. They are what get plugged into ethernet ports! They also feature a latch/clip that locks the ethernet cable into the port once it is plugged in.

+

RJ45 Boots

+

RJ45 Boot

+

RJ45 boots can be optionally used to protect the RJ45 connector. It provides insulation and prevents the cable from being breaking easily. They have to put slipped onto the cable before you put on the RJ45 connectors though!

+

RJ45 Cable Tester

+

RJ45 Cable Tester

+

RJ45 cable testers allow you to guarantee that you did the job correctly!

+

They have two pieces that separate from each other, and you plug each end of your crimped ethernet cable into the port on each piece. Then you turn it on and the cable tester will test the connection for all 8 pins. If there are any missing lights on any of the pins, it means that you messed up somewhere and have to restart!

+

How to Crimp an Ethernet Cable

+

Assuming you have a crimping kit and an ethernet cable that needs to be crimped, here are all the steps!

+

Step 0) Slip on the RJ45 boot (optional)

+

2 boots on a cable

+

Step 1) Strip the cable

+

cable being stripped by cable stripper

+
    +
  • Push the cable into the razor slot of the strip tool and turn it around the cable to make an even cut around the sheath. Careful not to nick the wires inside!
  • +
  • Unwrap the blue foil shielding and plastic to uncover the twisted wire pairs.
  • +
  • Push the copper grounding wire to the side. (Ignore the white string.)
  • +
+

Step 2) Organize the wires

+

In this step, you'll be taking the 8 colored wires inside the ethernet cable and putting them into the correct ordering of colors.

+

NOTEThis is the hardest part of crimping! The wires are small and are hard to control. Take your time and make sure you do this step correctly! Otherwise you might have to go back and restart.

+

Step 2.1) Untwist the wires

+

4 twisted pairs becoming 8 individual wires

+

There should be 4 pairs of wires: green, brown, orange, and blue. Each pair has a solid-colored wire and a striped-colored wire. Untwist these pairs and separate them into the 8 wires.

+

Step 2.2) Straighten out wires

+

After untwisting the wires, they are probably still kinked and look like they want to be twisted. In this step, you should carefully grab all the wires and try to straighten them out by pulling on them. This will prevent the wires from moving around later on.

+

WARNING Don't break off the wires!

+

Step 2.3) Lay out wires in order

+

Diagram showcasing ordering of RJ45 wires +8 wires in RJ45 color order

+

With your straightened out wires, put them into the correct order! Make sure that the wires are all flat and in line with each other.

+

The ordering for these wires is: +1. Striped orange +2. Solid orange +3. Striped green +4. Solid blue +5. Striped blue +6. Solid green +7. Striped brown +8. Solid brown

+

TIP After laying them out in order, straighten them out again as a group! This will help keep the wires together.

+

Step 2.4) Trim the wires

+

trimmed wires

+

Trim the wires evenly to about 1/2 inch in length using scissors or the blade of your crimping tool. You want to make sure you have enough room for the wires to reach the end of the RJ45 connector. But also try to have room for the shielding of the cable to be inserted into the connector too.

+

TIP You can put the wires side-by-side to the RJ45 connector to see how long you should cut it. Look at the next step to see what the final product looks like.

+

TIP If you don't have the shielding inside of the connector, it makes it easier for the wires to snap off later, which is bad.

+

TIP Make sure that you cut the wires evenly!

+

Step 3) Slide wires into RJ45 connector

+

Wires inserted into RJ45 connector with clip-side down

+

Carefully slide your 8 wires into the connector. Make sure that the clip is facing away from you! If it is really hard to slide it into the connector, you probably didn't straighten out the wires enough in step 2.2 or 2.3.

+

MORE INFO Inserting the wires with the clip facing away from you is the standard. However, you could technically do it in 'reverse' and insert the wires with the clip facing you, as long as you do it on both ends of the cable. You shouldn't do this in practice though because others would get confused when looking at your cable.

+

Step 4) Crimp it

+

Inserting into crimping tool and crimping

+

Push the RJ45 connector into the slot of your crimping tool for RJ45 connectors. The slot should be labeled something like "8P" for the 8-pin RJ45 connector that you're using.

+

In this step, you're doing the actual 'crimping' part and crimping/compressing/stabbing the 8 golden pins on the RJ45 connector into the 8 colored wires.

+

TIP Squeeze as hard as you can! You need to make sure that all 8 pins are crimped.

+

Step 5) Test it

+

Testing with cable tester, green light showing

+

Slide the two pieces of the tester apart and plug each of the cable ends into either piece. Turn the switch to “On” or “Slow.” If it's working, all 8 numbers should be flashing green.

+

If any of them are not showing green, it means something is wrong and you have to redo it! The RJ45 connector can't be reused once it's crimped, so you should just cut the end off and start back at step 1.

+

If everything is green, then you're done! If you had a cable boot, you can push the boots onto the RJ45 connector now.

+

Resources

+

Workshop Slides

+ +

Videos

+ +

Websites

+ +

Shopping

+
    +
  • Crimping Kit ($23)
      +
    • Comes with a nice case
    • +
    • Might need to buy your own batteries for cable tester
    • +
    +
  • +
  • Crimping Kit ($17)
      +
    • Might need to buy your own batteries for cable tester
    • +
    +
  • +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/learn/lte-networks/index.html b/learn/lte-networks/index.html new file mode 100644 index 0000000..14c698c --- /dev/null +++ b/learn/lte-networks/index.html @@ -0,0 +1,239 @@ + + + + + + + + LTE Networks - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

LTE Networks

+

Our network uses a 4G LTE network architecture. Understanding everything is a huge challenge, but understanding it at a high-level is very achievable.

+

Consider exploring all of these links as they all complement each other well!

+

Helpful videos

+ +

Helpful articles

+
    +
  • YaleBTS LTE Concepts
      +
    • Explains basically ALL the parts of the LTE architecture
    • +
    +
  • +
  • TutorialsPoint LTE Network Architecture
      +
    • Gives overview of LTE architecture
    • +
    • Not very detailed, but it's a good introduction to the individual components of LTE
    • +
    +
  • +
  • Open5GS Introduction
      +
    • Documentation for Open5GS, the repo that our networks rely on
    • +
    • This is the most applicable to our specific network because it's what we actually use.
    • +
    +
  • +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/learn/networking/index.html b/learn/networking/index.html new file mode 100644 index 0000000..f8c8fc6 --- /dev/null +++ b/learn/networking/index.html @@ -0,0 +1,192 @@ + + + + + + + + Networking - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Learn about Computer Networks

+

This page is currently in development. For now, please view our lesson on computer networks +from our Digital Stewards curriculum here.

+

TODO

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/learn/wireless-communication/index.html b/learn/wireless-communication/index.html new file mode 100644 index 0000000..65c22c3 --- /dev/null +++ b/learn/wireless-communication/index.html @@ -0,0 +1,379 @@ + + + + + + + + Wireless Communication - My Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Crash Course in Wireless Communication

+

Authored by: Dominick Ta +Last updated: June 18th, 2021

+

TODO:

+
    +
  • [ ] Expand on transmit/receiving in antennas section
  • +
  • [ ] Finish communication section
  • +
  • [ ] Finish protocols section
  • +
+

This is a crash course in wireless communication: the magical way that we humans send information (e.g. music, messages, text, videos) to each other over long distances, without a wire.

+

Wireless communication is primarily powered by radio waves, a physical thing that we as humans cannot see or touch. In this article you will gain a layman's understanding of how wireless communication works. Specifically, this article will cover the following broad topics:

+
    +
  • Radio waves and the physics behind it
  • +
  • Antennas
  • +
  • How data is communicated via radio waves
  • +
  • Examples of wireless communication technologies
  • +
+

This is not meant to be a comprehensive article, so there will be a lot of simplifications, analogies, and informal explanations. Please let me know at domta@cs.uw.edu if you see any inaccuracies, misconceptions, or misnomers.

+
+

Radio waves

+

Radio waves are just oscillations of energy that exist throughout our environment. They can be generated naturally by lightning, or artifically by equipment made by humans such as cell phones.

+

The technical definition of radio waves are that they are a form of 'electromagnetic radiation'. Other types of electromagnetic radiation include visible light (what we get from the sun), infrared, and X-rays.

+

Image of a radio surrounded by pulsating radio waves

+

How radio waves are created

+

To understand why radio waves are considered a type of electromagnetic radiation, it is helpful to know how we generate radio waves. This section is not important to fully understand, but it is good to skim to have a basic idea of how this stuff works.

+

We create radio waves by moving electrons back and forth within an electrically conductive object (an object made of material that allows electrons to move freely, such as metals).

+

This works because electrons have special properties:

+
    +
  • All electrons have 'electric fields' surrounding them that attracts and repels other charged particles.
  • +
  • All moving electrons produce 'magnetic fields' that are the basis for how magnets work.
  • +
  • Therefore, when electrons move there are two fields (electric & magnetic) present that combine to create creates an 'electromagnetic field'.
  • +
+

By moving an electron back and forth in an oscillating (repetitive) motion, these electromagnetic fields are constantly being disturbed/moved in a way that creates electromagnetic waves or equivalently, electromagnetic radiation.

+

In this GIF below, we see the charge of an antenna changing, representing electrons moving back and forth within the antenna. This creates pulsating electromagnetic waves. Note: in this particular GIF, it is actually only showing the pulsating electric fields.

+

Animated image of waves pulsating from an antenna

+

In this GIF below, we are able to fully see an electromagnetic wave with a 3D representation. The red component of the wave represents the electric field, while the blue component represents the magnetic field. Notice how they are perpendicular to each other! This is also why the GIF above didn't show both fields: it was a 2D representation! +3D gif of electromagnetic waves, showing both the electric fields and magnetic fields

+

In this section, we learned about radio waves. In essence, you can just think of radio waves as physical phenomenon that look like these sine waves: +GIF of a basic sinusoidal wave at a constant frequency

+

With the proper hardware and knowledge, we can create any type of wave we want! We can vary frequencies (how fast the waves go by), amplitudes (how tall/powerful the waves are), and phases (what position within the loop the wave is in).

+

Frequency & Wavelength

+

It turns out that a fundamental characteristic of any given radio wave is its frequency. Therefore, it's important to know what 'frequency' means, and how it relates to the concept of 'wavelength'.

+

Frequency is a measurement of how often something happens. In the case of radio waves, it measures how many cycles of a radio wave occurs in a certain amonut of time. Frequency is measured in the units of hertz (Hz), which represents cycles per second. If we had a radio wave that passed through 10 complete cycles in a minute, it would have a frequency of 0.16Hz (10 cycles per 60 seconds).

+

Wavelength measures the physical length of a single cycle in a radio wave. If we could see radio waves and measure it, and we saw that there was a distance of 10-feet between two of the peaks in a radio wave, then that radio wave would have a wavelength of 10 feet.

+

Image showcasing a wave with high frequency & short wavelength, and a wave with low frequency & long wavelength

+

In the image above, we can see a natural relationship between frequency and wavelength: a faster frequency means that you will have shorter wavelengths! Or conversely, longer wavelengths means that there is a slower frequency! When one value goes up, the other value has to go down; this is called an inverse relationship.

+

So if I told you that I had a radio wave with a very high frequency, you could figure out that my radio wave has very small wavelengths. And actually, if I told you exactly what frequency my radio waves travelled at, you'd be able to figure out the exact size of the wavelength!

+

For example, if I told you I had a radio wave with a frequency of 10Hz, you would be able to figure out that my radio wave has a wavelength of approximately 30,000,000 meters.

+

This is because radio waves are a physical thing, so they always travel through air at the same speed: the speed of light. This means we can consistently convert back and forth between frequency and wavelength with some basic algebra.

+

The basic equation, where c is the speed of light (3.0 x 10^8 m/s), is: +frequency (Hz) * wavelength (m) = c

+

So given a radio wave with a frequency of 10Hz, to solve for wavelength, I just need to take the speed of light and divide by 10! 30,000,000 divided by 10 gives a value of a wavelength of 30,000,000 meters.

+

To summarize this section, frequency and wavelength are important properties of radio waves. Although they describe different aspects of a radio wave, they are essentially synonymous because we can easily convert between the two.

+

When people are talking about radio waves, you may hear them talk in terms of frequencies (e.g. megahertz, gigahertz) or you may hear them talk in terms of wavelengths (e.g. meters).

+

In the next section, we'll see why exactly frequency is such an important characteristic of radio waves.

+

How we distinguish between radio waves

+

An important thing to remember about radio waves is that nowadays they surround us everywhere we go! Radio waves are powerful because they can go through obstacles like walls, and they can potentially propagate over huge distances at a relatively cheap cost.

+

Radio waves are used to communicate information in technologies such as GPS, WiFi, Bluetooth, cell phones, music radio stations, and more.

+

The problem with this is that these radio waves interfere with each other and combine together to become a jumbled mess! They are all co-existing within the same space, and they can't avoid each other.

+

In real life we don't get to have nice, simple, isolated radio wave like in the previous sections, we get a mix of radio waves coming all at once! And somehow, we have to find and narrow down the signal we care about.

+

GIF showcasing a bunch of random radio waves

+

Imagine you are at an airport and you're trying to talk to your friend, but it is super crowded and theres people talking and shouting all around you. It would be super hard to hear your friend and have a conversation!

+

(In this situation the people represent devices that use radio waves, the sound waves represent radio waves, and the voices & words represent the data we want to transmit)

+

But if your friend talks loud enough, even though your ear is full of noise from everyone else in the airport, you can still figure out what your friend is trying to tell you. This is because you're smart enough to recognize what your friend's voice sounds like and you can focus on that voice. We can do the same thing with radio waves of different frequencies!

+

(Being able to focus on your friend's specific voice is analogous to being able to focus on only radio waves of a specific frequency)

+

In the image below, we can see a red radio wave. This radio wave is actually the combination of 5 radio waves of different frequencies! In the blue, we see this same signal analyzed and split into the individual components, located at its respective frequencies. (Analogy: 5 people talking at the same time, what your ears hear is the red. Your brain recognizing that these are 5 different voices is the blue) +Two graphs, top graph is a red sinusoidal signal in the time domain, bottom graph is that same signal in the frequency domain showcasing 5 peaks at frequencies of 10, 20, 30, 40, and 50Hz.

+

The mathematical process of going from the red representation (the signal in the time domain) to the blue representation (the signal in the frequency domain) is called the "Discrete Fourier Transform". You may also hear about something called the "Fast Fourier Transform" which is the discrete fourier transform, but a very clever and fast way to calculate it.

+

(Intuitively, the DFT/FFT is essentially comparing a bunch of sine waves of varying frequencies to the signal detected, and calculating how similar they are. This would be like your brain iterating through all possible human voices and checking to see how strongly your ears hear that particular voice)

+

This means that even though there may be a jumbled mess of radio waves surrounding us at all times, if they are radio waves of different frequencies, we are able to distinguish between all these different frequencies using some old fashioned engineering.

+

This non-trivial fact is what allows our world to have so many different types of devices communicating wirelessly all at the same time! They are all using radio waves, all in the same space, but at different frequencies! This is why radio waves are identified by their frequency (or wavelength).

+

Summary

+

In this section on radio waves, we learned about what they are, the basics of how they're produced, frequency & wavelength, and how we distinguish between different radio waves co-existing in the same space. These are the fundamental concepts behind the physics of radio waves that engineers take advantage of.

+

In the next section on antennas, we will learn what they are and how they are used to efficiently propagate/send radio waves.

+

In another section, we will learn more about how exactly we manipulate radio waves to convey the information we want to send.

+

Antennas

+

We use antennas in our everyday lives, but most people don't know how they work. We use antennas on cars, on buildings, and even within our computers and phones!

+

How they work

+

Material properties

+

Antennas are all made up of electrically conductive material. This usually means antennas are made out of metals like copper.

+

Materials that are electrically conductive allow electrons to freely move throughout it. The opposite type of material would be electrically insulating material.

+

As a real-life analogy, imagine a typical swimming pool filled with water. A normal pool like this allows people to freely swim through it! In this analogy the water represents electrically conductive material and people represent electrons. Now imagine a piece of copper metal as being that pool of water. Because copper is electrically conductive, electrons can freely move through it!

+

An analogy for an electrically insulating material would be a swimming pool filled with jello/pudding/gelatin. If someone tried diving into that pool, they wouldn't get very far and it'd be super hard or impossible to swim through it. In this analogy the jello/pudding/gelatin represents electrically insulating material and people represent electrons. Now imagine a piece of plastic being that pool of water. Because plastic is electrically insulating, eletrons are mostly stuck where they are inside the plastic!

+

Taking advantage of moving electrons

+

Antennas need to be electrically conductive because they transmit and receive radio waves by taking advantage of the movement of electrons.

+

In the above section on "How radio waves are created" we learned that radio waves are generated by moving an electron back and forth in an oscillating (repetitive) motion. This oscillation of electrons occurs in the antennas.

+

If antennas weren't electrically conductive, these electrons wouldn't be able to move and create these radio waves!

+

How transmitting with an antenna works

+

In order to transmit with an antenna, the antenna needs to be connected to an electrical component that can control the movement of these electrons.

+

TODO: expand/clarify?

+

How receiving with an antenna works

+

Antennas receive radio signals passively. As the electromagnetic fields are manipulated in the environment around an antenna, the electrons in the antenna move accordingly (because of how the physics of it work). By monitoring the movement of these electrons, the respective radio wave can be captured.

+

TODO: expand/clarify?

+

Communicating with radio waves

+

How do we as humans harness this power of manipulating radio waves as communication? We agree on a bunch of different rules and conventions on how we will manipulating these radio waves to convey information efficiently and responsibly. In the United States, most of these rules are established and enforced by the Federal Communications Commission (FCC).

+

An analogy

+

To lay a conceptual foundation for this relatively abstract section, here is an analogy.

+

Suppose we have two friends with the simple names of "A" and "B" that want to talk to each other over a distance, but they cant see, hear, or touch each other over that distance. The only way method of communication they have is a really long piece of rope between them. So once they travel far away from each other, they will each be holding one end of the rope and will be trying to communicate.

+

But before they travel far away from each other, they need to talk to each other to figure out a set of rules of communication so that they can understand each other when they feel the rope moving.

+

When coming up with these rules for communication, one of the first things that A & B need to do is come up with a language. Because A & B really like computers, they chose the language of computers: binary. They chose this language because it is super simple and effective; it only has two letters (1 and 0) and you can send tons of information by being clever with 1s and 0s (thats what computers do).

+

Now the next thing that A & B needs to do is figure out how to use the rope to send these 1s and 0s. Here are some ideas they brainstormed together: +* If the rope is moving up and down (oscillating), then that is considered a 1. If the rope is not moving, then that is considered a 0. +* If the rope is oscillating super fast then that's a 1, but if the rope is oscillating slowly then that's a 0. +* If the rope is oscillating super fast then that's a 0, but if the rope is oscillating slowly then that's a 1. +* If the rope is oscillating with a big height then that's a 1, but if the rope is oscillating with small height then that's a 0.

+

A & B realized two properties of the movement of the rope that they could capture: (1) frequency, how fast the rope is oscillating, and (2) amplitude, how tall the rope is when its oscillating.

+

The term for manipulating these properties is "modulation". This makes sense because a dictionary definition of modulation is "to adjust"; modulation is just a fancy word for changing.

+

In this analogy, the material/medium of communication was the rope. But in our context, the material/medium of communication is radio waves. Both of these materials have frequency and amplitude that can be modulated to send information, and demodulated to receive that information.

+

In the next section, we'll learn about frequency and amplitude modulation. We'll also learn about some other types of modulation that don't have a direct real-life connection to ropes.

+

Modulation

+

Frequency modulation

+

Amplitude modulation

+

Other modulation schemes

+

Frequency allocation

+

Baseband signal and carrier signals

+

Bandwidth

+

Bands and Channels

+

Wireless Communication Protocols

+

In the previous section on "Communicating with Radio Waves", we learned how we are able to transmit messages wirelessly over radio waves via modulation.

+

We understood this through our analogy with friends A & B who came up with a way to send letters in their binary language (1 and 0) through a rope. Assuming they were able to do this successfully, they could send long streams of messages that look something like "101011100101010101010". But that's still not enough! They need to come up with a 'protocol' for deciphering what this message actually means!

+

For example, they could come up with a simple protocol for sending messages about how their day went. +* The first 3 letters would represent how their day went: "101" could mean that they had a good day, and "010" could mean that they had a bad day, and maybe "110" means that their day went okay. +* The next 3 letters would represent the weather: "100" could mean that it was sunny and "101" could mean that it was rainy.

+

We have these same types of protocols in real life that are much more complicated. They are carefully designed to ensure security (can people intercept messages?), reliability (what if the message gets messed up in certain places?), and efficiency (how much useful data can we send at a time?).

+

WiFi

+

Bluetooth

+

Cellular communication

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 9f425e8..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,6 +0,0 @@ -site_name: My Docs -theme: readthedocs - -plugins: - - search - - awesome-pages diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index fddea51..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -mkdocs -mkdocs-awesome-pages-plugin \ No newline at end of file diff --git a/search.html b/search.html new file mode 100644 index 0000000..71277bb --- /dev/null +++ b/search.html @@ -0,0 +1,181 @@ + + + + + + + + My Docs + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + + + diff --git a/search/lunr.js b/search/lunr.js new file mode 100644 index 0000000..aca0a16 --- /dev/null +++ b/search/lunr.js @@ -0,0 +1,3475 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + */ + +;(function(){ + +/** + * A convenience function for configuring and constructing + * a new lunr Index. + * + * A lunr.Builder instance is created and the pipeline setup + * with a trimmer, stop word filter and stemmer. + * + * This builder object is yielded to the configuration function + * that is passed as a parameter, allowing the list of fields + * and other builder parameters to be customised. + * + * All documents _must_ be added within the passed config function. + * + * @example + * var idx = lunr(function () { + * this.field('title') + * this.field('body') + * this.ref('id') + * + * documents.forEach(function (doc) { + * this.add(doc) + * }, this) + * }) + * + * @see {@link lunr.Builder} + * @see {@link lunr.Pipeline} + * @see {@link lunr.trimmer} + * @see {@link lunr.stopWordFilter} + * @see {@link lunr.stemmer} + * @namespace {function} lunr + */ +var lunr = function (config) { + var builder = new lunr.Builder + + builder.pipeline.add( + lunr.trimmer, + lunr.stopWordFilter, + lunr.stemmer + ) + + builder.searchPipeline.add( + lunr.stemmer + ) + + config.call(builder, builder) + return builder.build() +} + +lunr.version = "2.3.9" +/*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A namespace containing utils for the rest of the lunr library + * @namespace lunr.utils + */ +lunr.utils = {} + +/** + * Print a warning message to the console. + * + * @param {String} message The message to be printed. + * @memberOf lunr.utils + * @function + */ +lunr.utils.warn = (function (global) { + /* eslint-disable no-console */ + return function (message) { + if (global.console && console.warn) { + console.warn(message) + } + } + /* eslint-enable no-console */ +})(this) + +/** + * Convert an object to a string. + * + * In the case of `null` and `undefined` the function returns + * the empty string, in all other cases the result of calling + * `toString` on the passed object is returned. + * + * @param {Any} obj The object to convert to a string. + * @return {String} string representation of the passed object. + * @memberOf lunr.utils + */ +lunr.utils.asString = function (obj) { + if (obj === void 0 || obj === null) { + return "" + } else { + return obj.toString() + } +} + +/** + * Clones an object. + * + * Will create a copy of an existing object such that any mutations + * on the copy cannot affect the original. + * + * Only shallow objects are supported, passing a nested object to this + * function will cause a TypeError. + * + * Objects with primitives, and arrays of primitives are supported. + * + * @param {Object} obj The object to clone. + * @return {Object} a clone of the passed object. + * @throws {TypeError} when a nested object is passed. + * @memberOf Utils + */ +lunr.utils.clone = function (obj) { + if (obj === null || obj === undefined) { + return obj + } + + var clone = Object.create(null), + keys = Object.keys(obj) + + for (var i = 0; i < keys.length; i++) { + var key = keys[i], + val = obj[key] + + if (Array.isArray(val)) { + clone[key] = val.slice() + continue + } + + if (typeof val === 'string' || + typeof val === 'number' || + typeof val === 'boolean') { + clone[key] = val + continue + } + + throw new TypeError("clone is not deep and does not support nested objects") + } + + return clone +} +lunr.FieldRef = function (docRef, fieldName, stringValue) { + this.docRef = docRef + this.fieldName = fieldName + this._stringValue = stringValue +} + +lunr.FieldRef.joiner = "/" + +lunr.FieldRef.fromString = function (s) { + var n = s.indexOf(lunr.FieldRef.joiner) + + if (n === -1) { + throw "malformed field ref string" + } + + var fieldRef = s.slice(0, n), + docRef = s.slice(n + 1) + + return new lunr.FieldRef (docRef, fieldRef, s) +} + +lunr.FieldRef.prototype.toString = function () { + if (this._stringValue == undefined) { + this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef + } + + return this._stringValue +} +/*! + * lunr.Set + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A lunr set. + * + * @constructor + */ +lunr.Set = function (elements) { + this.elements = Object.create(null) + + if (elements) { + this.length = elements.length + + for (var i = 0; i < this.length; i++) { + this.elements[elements[i]] = true + } + } else { + this.length = 0 + } +} + +/** + * A complete set that contains all elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.complete = { + intersect: function (other) { + return other + }, + + union: function () { + return this + }, + + contains: function () { + return true + } +} + +/** + * An empty set that contains no elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.empty = { + intersect: function () { + return this + }, + + union: function (other) { + return other + }, + + contains: function () { + return false + } +} + +/** + * Returns true if this set contains the specified object. + * + * @param {object} object - Object whose presence in this set is to be tested. + * @returns {boolean} - True if this set contains the specified object. + */ +lunr.Set.prototype.contains = function (object) { + return !!this.elements[object] +} + +/** + * Returns a new set containing only the elements that are present in both + * this set and the specified set. + * + * @param {lunr.Set} other - set to intersect with this set. + * @returns {lunr.Set} a new set that is the intersection of this and the specified set. + */ + +lunr.Set.prototype.intersect = function (other) { + var a, b, elements, intersection = [] + + if (other === lunr.Set.complete) { + return this + } + + if (other === lunr.Set.empty) { + return other + } + + if (this.length < other.length) { + a = this + b = other + } else { + a = other + b = this + } + + elements = Object.keys(a.elements) + + for (var i = 0; i < elements.length; i++) { + var element = elements[i] + if (element in b.elements) { + intersection.push(element) + } + } + + return new lunr.Set (intersection) +} + +/** + * Returns a new set combining the elements of this and the specified set. + * + * @param {lunr.Set} other - set to union with this set. + * @return {lunr.Set} a new set that is the union of this and the specified set. + */ + +lunr.Set.prototype.union = function (other) { + if (other === lunr.Set.complete) { + return lunr.Set.complete + } + + if (other === lunr.Set.empty) { + return this + } + + return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements))) +} +/** + * A function to calculate the inverse document frequency for + * a posting. This is shared between the builder and the index + * + * @private + * @param {object} posting - The posting for a given term + * @param {number} documentCount - The total number of documents. + */ +lunr.idf = function (posting, documentCount) { + var documentsWithTerm = 0 + + for (var fieldName in posting) { + if (fieldName == '_index') continue // Ignore the term index, its not a field + documentsWithTerm += Object.keys(posting[fieldName]).length + } + + var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5) + + return Math.log(1 + Math.abs(x)) +} + +/** + * A token wraps a string representation of a token + * as it is passed through the text processing pipeline. + * + * @constructor + * @param {string} [str=''] - The string token being wrapped. + * @param {object} [metadata={}] - Metadata associated with this token. + */ +lunr.Token = function (str, metadata) { + this.str = str || "" + this.metadata = metadata || {} +} + +/** + * Returns the token string that is being wrapped by this object. + * + * @returns {string} + */ +lunr.Token.prototype.toString = function () { + return this.str +} + +/** + * A token update function is used when updating or optionally + * when cloning a token. + * + * @callback lunr.Token~updateFunction + * @param {string} str - The string representation of the token. + * @param {Object} metadata - All metadata associated with this token. + */ + +/** + * Applies the given function to the wrapped string token. + * + * @example + * token.update(function (str, metadata) { + * return str.toUpperCase() + * }) + * + * @param {lunr.Token~updateFunction} fn - A function to apply to the token string. + * @returns {lunr.Token} + */ +lunr.Token.prototype.update = function (fn) { + this.str = fn(this.str, this.metadata) + return this +} + +/** + * Creates a clone of this token. Optionally a function can be + * applied to the cloned token. + * + * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token. + * @returns {lunr.Token} + */ +lunr.Token.prototype.clone = function (fn) { + fn = fn || function (s) { return s } + return new lunr.Token (fn(this.str, this.metadata), this.metadata) +} +/*! + * lunr.tokenizer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A function for splitting a string into tokens ready to be inserted into + * the search index. Uses `lunr.tokenizer.separator` to split strings, change + * the value of this property to change how strings are split into tokens. + * + * This tokenizer will convert its parameter to a string by calling `toString` and + * then will split this string on the character in `lunr.tokenizer.separator`. + * Arrays will have their elements converted to strings and wrapped in a lunr.Token. + * + * Optional metadata can be passed to the tokenizer, this metadata will be cloned and + * added as metadata to every token that is created from the object to be tokenized. + * + * @static + * @param {?(string|object|object[])} obj - The object to convert into tokens + * @param {?object} metadata - Optional metadata to associate with every token + * @returns {lunr.Token[]} + * @see {@link lunr.Pipeline} + */ +lunr.tokenizer = function (obj, metadata) { + if (obj == null || obj == undefined) { + return [] + } + + if (Array.isArray(obj)) { + return obj.map(function (t) { + return new lunr.Token( + lunr.utils.asString(t).toLowerCase(), + lunr.utils.clone(metadata) + ) + }) + } + + var str = obj.toString().toLowerCase(), + len = str.length, + tokens = [] + + for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) { + var char = str.charAt(sliceEnd), + sliceLength = sliceEnd - sliceStart + + if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) { + + if (sliceLength > 0) { + var tokenMetadata = lunr.utils.clone(metadata) || {} + tokenMetadata["position"] = [sliceStart, sliceLength] + tokenMetadata["index"] = tokens.length + + tokens.push( + new lunr.Token ( + str.slice(sliceStart, sliceEnd), + tokenMetadata + ) + ) + } + + sliceStart = sliceEnd + 1 + } + + } + + return tokens +} + +/** + * The separator used to split a string into tokens. Override this property to change the behaviour of + * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens. + * + * @static + * @see lunr.tokenizer + */ +lunr.tokenizer.separator = /[\s\-]+/ +/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ +lunr.Pipeline = function () { + this._stack = [] +} + +lunr.Pipeline.registeredFunctions = Object.create(null) + +/** + * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token + * string as well as all known metadata. A pipeline function can mutate the token string + * or mutate (or add) metadata for a given token. + * + * A pipeline function can indicate that the passed token should be discarded by returning + * null, undefined or an empty string. This token will not be passed to any downstream pipeline + * functions and will not be added to the index. + * + * Multiple tokens can be returned by returning an array of tokens. Each token will be passed + * to any downstream pipeline functions and all will returned tokens will be added to the index. + * + * Any number of pipeline functions may be chained together using a lunr.Pipeline. + * + * @interface lunr.PipelineFunction + * @param {lunr.Token} token - A token from the document being processed. + * @param {number} i - The index of this token in the complete list of tokens for this document/field. + * @param {lunr.Token[]} tokens - All tokens for this document/field. + * @returns {(?lunr.Token|lunr.Token[])} + */ + +/** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @param {String} label - The label to register this function with + */ +lunr.Pipeline.registerFunction = function (fn, label) { + if (label in this.registeredFunctions) { + lunr.utils.warn('Overwriting existing registered function: ' + label) + } + + fn.label = label + lunr.Pipeline.registeredFunctions[fn.label] = fn +} + +/** + * Warns if the function is not registered as a Pipeline function. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @private + */ +lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { + var isRegistered = fn.label && (fn.label in this.registeredFunctions) + + if (!isRegistered) { + lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) + } +} + +/** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised - The serialised pipeline to load. + * @returns {lunr.Pipeline} + */ +lunr.Pipeline.load = function (serialised) { + var pipeline = new lunr.Pipeline + + serialised.forEach(function (fnName) { + var fn = lunr.Pipeline.registeredFunctions[fnName] + + if (fn) { + pipeline.add(fn) + } else { + throw new Error('Cannot load unregistered function: ' + fnName) + } + }) + + return pipeline +} + +/** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline. + */ +lunr.Pipeline.prototype.add = function () { + var fns = Array.prototype.slice.call(arguments) + + fns.forEach(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + this._stack.push(fn) + }, this) +} + +/** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.after = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + pos = pos + 1 + this._stack.splice(pos, 0, newFn) +} + +/** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.before = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + this._stack.splice(pos, 0, newFn) +} + +/** + * Removes a function from the pipeline. + * + * @param {lunr.PipelineFunction} fn The function to remove from the pipeline. + */ +lunr.Pipeline.prototype.remove = function (fn) { + var pos = this._stack.indexOf(fn) + if (pos == -1) { + return + } + + this._stack.splice(pos, 1) +} + +/** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + */ +lunr.Pipeline.prototype.run = function (tokens) { + var stackLength = this._stack.length + + for (var i = 0; i < stackLength; i++) { + var fn = this._stack[i] + var memo = [] + + for (var j = 0; j < tokens.length; j++) { + var result = fn(tokens[j], j, tokens) + + if (result === null || result === void 0 || result === '') continue + + if (Array.isArray(result)) { + for (var k = 0; k < result.length; k++) { + memo.push(result[k]) + } + } else { + memo.push(result) + } + } + + tokens = memo + } + + return tokens +} + +/** + * Convenience method for passing a string through a pipeline and getting + * strings out. This method takes care of wrapping the passed string in a + * token and mapping the resulting tokens back to strings. + * + * @param {string} str - The string to pass through the pipeline. + * @param {?object} metadata - Optional metadata to associate with the token + * passed to the pipeline. + * @returns {string[]} + */ +lunr.Pipeline.prototype.runString = function (str, metadata) { + var token = new lunr.Token (str, metadata) + + return this.run([token]).map(function (t) { + return t.toString() + }) +} + +/** + * Resets the pipeline by removing any existing processors. + * + */ +lunr.Pipeline.prototype.reset = function () { + this._stack = [] +} + +/** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + */ +lunr.Pipeline.prototype.toJSON = function () { + return this._stack.map(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + + return fn.label + }) +} +/*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A vector is used to construct the vector space of documents and queries. These + * vectors support operations to determine the similarity between two documents or + * a document and a query. + * + * Normally no parameters are required for initializing a vector, but in the case of + * loading a previously dumped vector the raw elements can be provided to the constructor. + * + * For performance reasons vectors are implemented with a flat array, where an elements + * index is immediately followed by its value. E.g. [index, value, index, value]. This + * allows the underlying array to be as sparse as possible and still offer decent + * performance when being used for vector calculations. + * + * @constructor + * @param {Number[]} [elements] - The flat list of element index and element value pairs. + */ +lunr.Vector = function (elements) { + this._magnitude = 0 + this.elements = elements || [] +} + + +/** + * Calculates the position within the vector to insert a given index. + * + * This is used internally by insert and upsert. If there are duplicate indexes then + * the position is returned as if the value for that index were to be updated, but it + * is the callers responsibility to check whether there is a duplicate at that index + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @returns {Number} + */ +lunr.Vector.prototype.positionForIndex = function (index) { + // For an empty vector the tuple can be inserted at the beginning + if (this.elements.length == 0) { + return 0 + } + + var start = 0, + end = this.elements.length / 2, + sliceLength = end - start, + pivotPoint = Math.floor(sliceLength / 2), + pivotIndex = this.elements[pivotPoint * 2] + + while (sliceLength > 1) { + if (pivotIndex < index) { + start = pivotPoint + } + + if (pivotIndex > index) { + end = pivotPoint + } + + if (pivotIndex == index) { + break + } + + sliceLength = end - start + pivotPoint = start + Math.floor(sliceLength / 2) + pivotIndex = this.elements[pivotPoint * 2] + } + + if (pivotIndex == index) { + return pivotPoint * 2 + } + + if (pivotIndex > index) { + return pivotPoint * 2 + } + + if (pivotIndex < index) { + return (pivotPoint + 1) * 2 + } +} + +/** + * Inserts an element at an index within the vector. + * + * Does not allow duplicates, will throw an error if there is already an entry + * for this index. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + */ +lunr.Vector.prototype.insert = function (insertIdx, val) { + this.upsert(insertIdx, val, function () { + throw "duplicate index" + }) +} + +/** + * Inserts or updates an existing index within the vector. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + * @param {function} fn - A function that is called for updates, the existing value and the + * requested value are passed as arguments + */ +lunr.Vector.prototype.upsert = function (insertIdx, val, fn) { + this._magnitude = 0 + var position = this.positionForIndex(insertIdx) + + if (this.elements[position] == insertIdx) { + this.elements[position + 1] = fn(this.elements[position + 1], val) + } else { + this.elements.splice(position, 0, insertIdx, val) + } +} + +/** + * Calculates the magnitude of this vector. + * + * @returns {Number} + */ +lunr.Vector.prototype.magnitude = function () { + if (this._magnitude) return this._magnitude + + var sumOfSquares = 0, + elementsLength = this.elements.length + + for (var i = 1; i < elementsLength; i += 2) { + var val = this.elements[i] + sumOfSquares += val * val + } + + return this._magnitude = Math.sqrt(sumOfSquares) +} + +/** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector - The vector to compute the dot product with. + * @returns {Number} + */ +lunr.Vector.prototype.dot = function (otherVector) { + var dotProduct = 0, + a = this.elements, b = otherVector.elements, + aLen = a.length, bLen = b.length, + aVal = 0, bVal = 0, + i = 0, j = 0 + + while (i < aLen && j < bLen) { + aVal = a[i], bVal = b[j] + if (aVal < bVal) { + i += 2 + } else if (aVal > bVal) { + j += 2 + } else if (aVal == bVal) { + dotProduct += a[i + 1] * b[j + 1] + i += 2 + j += 2 + } + } + + return dotProduct +} + +/** + * Calculates the similarity between this vector and another vector. + * + * @param {lunr.Vector} otherVector - The other vector to calculate the + * similarity with. + * @returns {Number} + */ +lunr.Vector.prototype.similarity = function (otherVector) { + return this.dot(otherVector) / this.magnitude() || 0 +} + +/** + * Converts the vector to an array of the elements within the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toArray = function () { + var output = new Array (this.elements.length / 2) + + for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) { + output[j] = this.elements[i] + } + + return output +} + +/** + * A JSON serializable representation of the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toJSON = function () { + return this.elements +} +/* eslint-disable */ +/*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + +/** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartarus.org/~martin + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token - The string to stem + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + * @function + */ +lunr.stemmer = (function(){ + var step2list = { + "ational" : "ate", + "tional" : "tion", + "enci" : "ence", + "anci" : "ance", + "izer" : "ize", + "bli" : "ble", + "alli" : "al", + "entli" : "ent", + "eli" : "e", + "ousli" : "ous", + "ization" : "ize", + "ation" : "ate", + "ator" : "ate", + "alism" : "al", + "iveness" : "ive", + "fulness" : "ful", + "ousness" : "ous", + "aliti" : "al", + "iviti" : "ive", + "biliti" : "ble", + "logi" : "log" + }, + + step3list = { + "icate" : "ic", + "ative" : "", + "alize" : "al", + "iciti" : "ic", + "ical" : "ic", + "ful" : "", + "ness" : "" + }, + + c = "[^aeiou]", // consonant + v = "[aeiouy]", // vowel + C = c + "[^aeiouy]*", // consonant sequence + V = v + "[aeiou]*", // vowel sequence + + mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 + meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 + mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 + s_v = "^(" + C + ")?" + v; // vowel in stem + + var re_mgr0 = new RegExp(mgr0); + var re_mgr1 = new RegExp(mgr1); + var re_meq1 = new RegExp(meq1); + var re_s_v = new RegExp(s_v); + + var re_1a = /^(.+?)(ss|i)es$/; + var re2_1a = /^(.+?)([^s])s$/; + var re_1b = /^(.+?)eed$/; + var re2_1b = /^(.+?)(ed|ing)$/; + var re_1b_2 = /.$/; + var re2_1b_2 = /(at|bl|iz)$/; + var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); + var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var re_1c = /^(.+?[^aeiou])y$/; + var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + + var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + + var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + var re2_4 = /^(.+?)(s|t)(ion)$/; + + var re_5 = /^(.+?)e$/; + var re_5_1 = /ll$/; + var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var porterStemmer = function porterStemmer(w) { + var stem, + suffix, + firstch, + re, + re2, + re3, + re4; + + if (w.length < 3) { return w; } + + firstch = w.substr(0,1); + if (firstch == "y") { + w = firstch.toUpperCase() + w.substr(1); + } + + // Step 1a + re = re_1a + re2 = re2_1a; + + if (re.test(w)) { w = w.replace(re,"$1$2"); } + else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + + // Step 1b + re = re_1b; + re2 = re2_1b; + if (re.test(w)) { + var fp = re.exec(w); + re = re_mgr0; + if (re.test(fp[1])) { + re = re_1b_2; + w = w.replace(re,""); + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = re_s_v; + if (re2.test(stem)) { + w = stem; + re2 = re2_1b_2; + re3 = re3_1b_2; + re4 = re4_1b_2; + if (re2.test(w)) { w = w + "e"; } + else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } + else if (re4.test(w)) { w = w + "e"; } + } + } + + // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) + re = re_1c; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem + "i"; + } + + // Step 2 + re = re_2; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step2list[suffix]; + } + } + + // Step 3 + re = re_3; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step3list[suffix]; + } + } + + // Step 4 + re = re_4; + re2 = re2_4; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + if (re.test(stem)) { + w = stem; + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = re_mgr1; + if (re2.test(stem)) { + w = stem; + } + } + + // Step 5 + re = re_5; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + re2 = re_meq1; + re3 = re3_5; + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { + w = stem; + } + } + + re = re_5_1; + re2 = re_mgr1; + if (re.test(w) && re2.test(w)) { + re = re_1b_2; + w = w.replace(re,""); + } + + // and turn initial Y back to y + + if (firstch == "y") { + w = firstch.toLowerCase() + w.substr(1); + } + + return w; + }; + + return function (token) { + return token.update(porterStemmer); + } +})(); + +lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') +/*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.generateStopWordFilter builds a stopWordFilter function from the provided + * list of stop words. + * + * The built in lunr.stopWordFilter is built using this generator and can be used + * to generate custom stopWordFilters for applications or non English languages. + * + * @function + * @param {Array} token The token to pass through the filter + * @returns {lunr.PipelineFunction} + * @see lunr.Pipeline + * @see lunr.stopWordFilter + */ +lunr.generateStopWordFilter = function (stopWords) { + var words = stopWords.reduce(function (memo, stopWord) { + memo[stopWord] = stopWord + return memo + }, {}) + + return function (token) { + if (token && words[token.toString()] !== token.toString()) return token + } +} + +/** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @function + * @implements {lunr.PipelineFunction} + * @params {lunr.Token} token - A token to check for being a stop word. + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + */ +lunr.stopWordFilter = lunr.generateStopWordFilter([ + 'a', + 'able', + 'about', + 'across', + 'after', + 'all', + 'almost', + 'also', + 'am', + 'among', + 'an', + 'and', + 'any', + 'are', + 'as', + 'at', + 'be', + 'because', + 'been', + 'but', + 'by', + 'can', + 'cannot', + 'could', + 'dear', + 'did', + 'do', + 'does', + 'either', + 'else', + 'ever', + 'every', + 'for', + 'from', + 'get', + 'got', + 'had', + 'has', + 'have', + 'he', + 'her', + 'hers', + 'him', + 'his', + 'how', + 'however', + 'i', + 'if', + 'in', + 'into', + 'is', + 'it', + 'its', + 'just', + 'least', + 'let', + 'like', + 'likely', + 'may', + 'me', + 'might', + 'most', + 'must', + 'my', + 'neither', + 'no', + 'nor', + 'not', + 'of', + 'off', + 'often', + 'on', + 'only', + 'or', + 'other', + 'our', + 'own', + 'rather', + 'said', + 'say', + 'says', + 'she', + 'should', + 'since', + 'so', + 'some', + 'than', + 'that', + 'the', + 'their', + 'them', + 'then', + 'there', + 'these', + 'they', + 'this', + 'tis', + 'to', + 'too', + 'twas', + 'us', + 'wants', + 'was', + 'we', + 'were', + 'what', + 'when', + 'where', + 'which', + 'while', + 'who', + 'whom', + 'why', + 'will', + 'with', + 'would', + 'yet', + 'you', + 'your' +]) + +lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') +/*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.trimmer is a pipeline function for trimming non word + * characters from the beginning and end of tokens before they + * enter the index. + * + * This implementation may not work correctly for non latin + * characters and should either be removed or adapted for use + * with languages with non-latin characters. + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token The token to pass through the filter + * @returns {lunr.Token} + * @see lunr.Pipeline + */ +lunr.trimmer = function (token) { + return token.update(function (s) { + return s.replace(/^\W+/, '').replace(/\W+$/, '') + }) +} + +lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') +/*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A token set is used to store the unique list of all tokens + * within an index. Token sets are also used to represent an + * incoming query to the index, this query token set and index + * token set are then intersected to find which tokens to look + * up in the inverted index. + * + * A token set can hold multiple tokens, as in the case of the + * index token set, or it can hold a single token as in the + * case of a simple query token set. + * + * Additionally token sets are used to perform wildcard matching. + * Leading, contained and trailing wildcards are supported, and + * from this edit distance matching can also be provided. + * + * Token sets are implemented as a minimal finite state automata, + * where both common prefixes and suffixes are shared between tokens. + * This helps to reduce the space used for storing the token set. + * + * @constructor + */ +lunr.TokenSet = function () { + this.final = false + this.edges = {} + this.id = lunr.TokenSet._nextId + lunr.TokenSet._nextId += 1 +} + +/** + * Keeps track of the next, auto increment, identifier to assign + * to a new tokenSet. + * + * TokenSets require a unique identifier to be correctly minimised. + * + * @private + */ +lunr.TokenSet._nextId = 1 + +/** + * Creates a TokenSet instance from the given sorted array of words. + * + * @param {String[]} arr - A sorted array of strings to create the set from. + * @returns {lunr.TokenSet} + * @throws Will throw an error if the input array is not sorted. + */ +lunr.TokenSet.fromArray = function (arr) { + var builder = new lunr.TokenSet.Builder + + for (var i = 0, len = arr.length; i < len; i++) { + builder.insert(arr[i]) + } + + builder.finish() + return builder.root +} + +/** + * Creates a token set from a query clause. + * + * @private + * @param {Object} clause - A single clause from lunr.Query. + * @param {string} clause.term - The query clause term. + * @param {number} [clause.editDistance] - The optional edit distance for the term. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromClause = function (clause) { + if ('editDistance' in clause) { + return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance) + } else { + return lunr.TokenSet.fromString(clause.term) + } +} + +/** + * Creates a token set representing a single string with a specified + * edit distance. + * + * Insertions, deletions, substitutions and transpositions are each + * treated as an edit distance of 1. + * + * Increasing the allowed edit distance will have a dramatic impact + * on the performance of both creating and intersecting these TokenSets. + * It is advised to keep the edit distance less than 3. + * + * @param {string} str - The string to create the token set from. + * @param {number} editDistance - The allowed edit distance to match. + * @returns {lunr.Vector} + */ +lunr.TokenSet.fromFuzzyString = function (str, editDistance) { + var root = new lunr.TokenSet + + var stack = [{ + node: root, + editsRemaining: editDistance, + str: str + }] + + while (stack.length) { + var frame = stack.pop() + + // no edit + if (frame.str.length > 0) { + var char = frame.str.charAt(0), + noEditNode + + if (char in frame.node.edges) { + noEditNode = frame.node.edges[char] + } else { + noEditNode = new lunr.TokenSet + frame.node.edges[char] = noEditNode + } + + if (frame.str.length == 1) { + noEditNode.final = true + } + + stack.push({ + node: noEditNode, + editsRemaining: frame.editsRemaining, + str: frame.str.slice(1) + }) + } + + if (frame.editsRemaining == 0) { + continue + } + + // insertion + if ("*" in frame.node.edges) { + var insertionNode = frame.node.edges["*"] + } else { + var insertionNode = new lunr.TokenSet + frame.node.edges["*"] = insertionNode + } + + if (frame.str.length == 0) { + insertionNode.final = true + } + + stack.push({ + node: insertionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str + }) + + // deletion + // can only do a deletion if we have enough edits remaining + // and if there are characters left to delete in the string + if (frame.str.length > 1) { + stack.push({ + node: frame.node, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // deletion + // just removing the last character from the str + if (frame.str.length == 1) { + frame.node.final = true + } + + // substitution + // can only do a substitution if we have enough edits remaining + // and if there are characters left to substitute + if (frame.str.length >= 1) { + if ("*" in frame.node.edges) { + var substitutionNode = frame.node.edges["*"] + } else { + var substitutionNode = new lunr.TokenSet + frame.node.edges["*"] = substitutionNode + } + + if (frame.str.length == 1) { + substitutionNode.final = true + } + + stack.push({ + node: substitutionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // transposition + // can only do a transposition if there are edits remaining + // and there are enough characters to transpose + if (frame.str.length > 1) { + var charA = frame.str.charAt(0), + charB = frame.str.charAt(1), + transposeNode + + if (charB in frame.node.edges) { + transposeNode = frame.node.edges[charB] + } else { + transposeNode = new lunr.TokenSet + frame.node.edges[charB] = transposeNode + } + + if (frame.str.length == 1) { + transposeNode.final = true + } + + stack.push({ + node: transposeNode, + editsRemaining: frame.editsRemaining - 1, + str: charA + frame.str.slice(2) + }) + } + } + + return root +} + +/** + * Creates a TokenSet from a string. + * + * The string may contain one or more wildcard characters (*) + * that will allow wildcard matching when intersecting with + * another TokenSet. + * + * @param {string} str - The string to create a TokenSet from. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromString = function (str) { + var node = new lunr.TokenSet, + root = node + + /* + * Iterates through all characters within the passed string + * appending a node for each character. + * + * When a wildcard character is found then a self + * referencing edge is introduced to continually match + * any number of any characters. + */ + for (var i = 0, len = str.length; i < len; i++) { + var char = str[i], + final = (i == len - 1) + + if (char == "*") { + node.edges[char] = node + node.final = final + + } else { + var next = new lunr.TokenSet + next.final = final + + node.edges[char] = next + node = next + } + } + + return root +} + +/** + * Converts this TokenSet into an array of strings + * contained within the TokenSet. + * + * This is not intended to be used on a TokenSet that + * contains wildcards, in these cases the results are + * undefined and are likely to cause an infinite loop. + * + * @returns {string[]} + */ +lunr.TokenSet.prototype.toArray = function () { + var words = [] + + var stack = [{ + prefix: "", + node: this + }] + + while (stack.length) { + var frame = stack.pop(), + edges = Object.keys(frame.node.edges), + len = edges.length + + if (frame.node.final) { + /* In Safari, at this point the prefix is sometimes corrupted, see: + * https://github.com/olivernn/lunr.js/issues/279 Calling any + * String.prototype method forces Safari to "cast" this string to what + * it's supposed to be, fixing the bug. */ + frame.prefix.charAt(0) + words.push(frame.prefix) + } + + for (var i = 0; i < len; i++) { + var edge = edges[i] + + stack.push({ + prefix: frame.prefix.concat(edge), + node: frame.node.edges[edge] + }) + } + } + + return words +} + +/** + * Generates a string representation of a TokenSet. + * + * This is intended to allow TokenSets to be used as keys + * in objects, largely to aid the construction and minimisation + * of a TokenSet. As such it is not designed to be a human + * friendly representation of the TokenSet. + * + * @returns {string} + */ +lunr.TokenSet.prototype.toString = function () { + // NOTE: Using Object.keys here as this.edges is very likely + // to enter 'hash-mode' with many keys being added + // + // avoiding a for-in loop here as it leads to the function + // being de-optimised (at least in V8). From some simple + // benchmarks the performance is comparable, but allowing + // V8 to optimize may mean easy performance wins in the future. + + if (this._str) { + return this._str + } + + var str = this.final ? '1' : '0', + labels = Object.keys(this.edges).sort(), + len = labels.length + + for (var i = 0; i < len; i++) { + var label = labels[i], + node = this.edges[label] + + str = str + label + node.id + } + + return str +} + +/** + * Returns a new TokenSet that is the intersection of + * this TokenSet and the passed TokenSet. + * + * This intersection will take into account any wildcards + * contained within the TokenSet. + * + * @param {lunr.TokenSet} b - An other TokenSet to intersect with. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.prototype.intersect = function (b) { + var output = new lunr.TokenSet, + frame = undefined + + var stack = [{ + qNode: b, + output: output, + node: this + }] + + while (stack.length) { + frame = stack.pop() + + // NOTE: As with the #toString method, we are using + // Object.keys and a for loop instead of a for-in loop + // as both of these objects enter 'hash' mode, causing + // the function to be de-optimised in V8 + var qEdges = Object.keys(frame.qNode.edges), + qLen = qEdges.length, + nEdges = Object.keys(frame.node.edges), + nLen = nEdges.length + + for (var q = 0; q < qLen; q++) { + var qEdge = qEdges[q] + + for (var n = 0; n < nLen; n++) { + var nEdge = nEdges[n] + + if (nEdge == qEdge || qEdge == '*') { + var node = frame.node.edges[nEdge], + qNode = frame.qNode.edges[qEdge], + final = node.final && qNode.final, + next = undefined + + if (nEdge in frame.output.edges) { + // an edge already exists for this character + // no need to create a new node, just set the finality + // bit unless this node is already final + next = frame.output.edges[nEdge] + next.final = next.final || final + + } else { + // no edge exists yet, must create one + // set the finality bit and insert it + // into the output + next = new lunr.TokenSet + next.final = final + frame.output.edges[nEdge] = next + } + + stack.push({ + qNode: qNode, + output: next, + node: node + }) + } + } + } + } + + return output +} +lunr.TokenSet.Builder = function () { + this.previousWord = "" + this.root = new lunr.TokenSet + this.uncheckedNodes = [] + this.minimizedNodes = {} +} + +lunr.TokenSet.Builder.prototype.insert = function (word) { + var node, + commonPrefix = 0 + + if (word < this.previousWord) { + throw new Error ("Out of order word insertion") + } + + for (var i = 0; i < word.length && i < this.previousWord.length; i++) { + if (word[i] != this.previousWord[i]) break + commonPrefix++ + } + + this.minimize(commonPrefix) + + if (this.uncheckedNodes.length == 0) { + node = this.root + } else { + node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child + } + + for (var i = commonPrefix; i < word.length; i++) { + var nextNode = new lunr.TokenSet, + char = word[i] + + node.edges[char] = nextNode + + this.uncheckedNodes.push({ + parent: node, + char: char, + child: nextNode + }) + + node = nextNode + } + + node.final = true + this.previousWord = word +} + +lunr.TokenSet.Builder.prototype.finish = function () { + this.minimize(0) +} + +lunr.TokenSet.Builder.prototype.minimize = function (downTo) { + for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) { + var node = this.uncheckedNodes[i], + childKey = node.child.toString() + + if (childKey in this.minimizedNodes) { + node.parent.edges[node.char] = this.minimizedNodes[childKey] + } else { + // Cache the key for this node since + // we know it can't change anymore + node.child._str = childKey + + this.minimizedNodes[childKey] = node.child + } + + this.uncheckedNodes.pop() + } +} +/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * An index contains the built index of all documents and provides a query interface + * to the index. + * + * Usually instances of lunr.Index will not be created using this constructor, instead + * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be + * used to load previously built and serialized indexes. + * + * @constructor + * @param {Object} attrs - The attributes of the built search index. + * @param {Object} attrs.invertedIndex - An index of term/field to document reference. + * @param {Object} attrs.fieldVectors - Field vectors + * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens. + * @param {string[]} attrs.fields - The names of indexed document fields. + * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms. + */ +lunr.Index = function (attrs) { + this.invertedIndex = attrs.invertedIndex + this.fieldVectors = attrs.fieldVectors + this.tokenSet = attrs.tokenSet + this.fields = attrs.fields + this.pipeline = attrs.pipeline +} + +/** + * A result contains details of a document matching a search query. + * @typedef {Object} lunr.Index~Result + * @property {string} ref - The reference of the document this result represents. + * @property {number} score - A number between 0 and 1 representing how similar this document is to the query. + * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match. + */ + +/** + * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple + * query language which itself is parsed into an instance of lunr.Query. + * + * For programmatically building queries it is advised to directly use lunr.Query, the query language + * is best used for human entered text rather than program generated text. + * + * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported + * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello' + * or 'world', though those that contain both will rank higher in the results. + * + * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can + * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding + * wildcards will increase the number of documents that will be found but can also have a negative + * impact on query performance, especially with wildcards at the beginning of a term. + * + * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term + * hello in the title field will match this query. Using a field not present in the index will lead + * to an error being thrown. + * + * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term + * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported + * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2. + * Avoid large values for edit distance to improve query performance. + * + * Each term also supports a presence modifier. By default a term's presence in document is optional, however + * this can be changed to either required or prohibited. For a term's presence to be required in a document the + * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and + * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not + * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'. + * + * To escape special characters the backslash character '\' can be used, this allows searches to include + * characters that would normally be considered modifiers, e.g. `foo\~2` will search for a term "foo~2" instead + * of attempting to apply a boost of 2 to the search term "foo". + * + * @typedef {string} lunr.Index~QueryString + * @example Simple single term query + * hello + * @example Multiple term query + * hello world + * @example term scoped to a field + * title:hello + * @example term with a boost of 10 + * hello^10 + * @example term with an edit distance of 2 + * hello~2 + * @example terms with presence modifiers + * -foo +bar baz + */ + +/** + * Performs a search against the index using lunr query syntax. + * + * Results will be returned sorted by their score, the most relevant results + * will be returned first. For details on how the score is calculated, please see + * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}. + * + * For more programmatic querying use lunr.Index#query. + * + * @param {lunr.Index~QueryString} queryString - A string containing a lunr query. + * @throws {lunr.QueryParseError} If the passed query string cannot be parsed. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.search = function (queryString) { + return this.query(function (query) { + var parser = new lunr.QueryParser(queryString, query) + parser.parse() + }) +} + +/** + * A query builder callback provides a query object to be used to express + * the query to perform on the index. + * + * @callback lunr.Index~queryBuilder + * @param {lunr.Query} query - The query object to build up. + * @this lunr.Query + */ + +/** + * Performs a query against the index using the yielded lunr.Query object. + * + * If performing programmatic queries against the index, this method is preferred + * over lunr.Index#search so as to avoid the additional query parsing overhead. + * + * A query object is yielded to the supplied function which should be used to + * express the query to be run against the index. + * + * Note that although this function takes a callback parameter it is _not_ an + * asynchronous operation, the callback is just yielded a query object to be + * customized. + * + * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.query = function (fn) { + // for each query clause + // * process terms + // * expand terms from token set + // * find matching documents and metadata + // * get document vectors + // * score documents + + var query = new lunr.Query(this.fields), + matchingFields = Object.create(null), + queryVectors = Object.create(null), + termFieldCache = Object.create(null), + requiredMatches = Object.create(null), + prohibitedMatches = Object.create(null) + + /* + * To support field level boosts a query vector is created per + * field. An empty vector is eagerly created to support negated + * queries. + */ + for (var i = 0; i < this.fields.length; i++) { + queryVectors[this.fields[i]] = new lunr.Vector + } + + fn.call(query, query) + + for (var i = 0; i < query.clauses.length; i++) { + /* + * Unless the pipeline has been disabled for this term, which is + * the case for terms with wildcards, we need to pass the clause + * term through the search pipeline. A pipeline returns an array + * of processed terms. Pipeline functions may expand the passed + * term, which means we may end up performing multiple index lookups + * for a single query term. + */ + var clause = query.clauses[i], + terms = null, + clauseMatches = lunr.Set.empty + + if (clause.usePipeline) { + terms = this.pipeline.runString(clause.term, { + fields: clause.fields + }) + } else { + terms = [clause.term] + } + + for (var m = 0; m < terms.length; m++) { + var term = terms[m] + + /* + * Each term returned from the pipeline needs to use the same query + * clause object, e.g. the same boost and or edit distance. The + * simplest way to do this is to re-use the clause object but mutate + * its term property. + */ + clause.term = term + + /* + * From the term in the clause we create a token set which will then + * be used to intersect the indexes token set to get a list of terms + * to lookup in the inverted index + */ + var termTokenSet = lunr.TokenSet.fromClause(clause), + expandedTerms = this.tokenSet.intersect(termTokenSet).toArray() + + /* + * If a term marked as required does not exist in the tokenSet it is + * impossible for the search to return any matches. We set all the field + * scoped required matches set to empty and stop examining any further + * clauses. + */ + if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = lunr.Set.empty + } + + break + } + + for (var j = 0; j < expandedTerms.length; j++) { + /* + * For each term get the posting and termIndex, this is required for + * building the query vector. + */ + var expandedTerm = expandedTerms[j], + posting = this.invertedIndex[expandedTerm], + termIndex = posting._index + + for (var k = 0; k < clause.fields.length; k++) { + /* + * For each field that this query term is scoped by (by default + * all fields are in scope) we need to get all the document refs + * that have this term in that field. + * + * The posting is the entry in the invertedIndex for the matching + * term from above. + */ + var field = clause.fields[k], + fieldPosting = posting[field], + matchingDocumentRefs = Object.keys(fieldPosting), + termField = expandedTerm + "/" + field, + matchingDocumentsSet = new lunr.Set(matchingDocumentRefs) + + /* + * if the presence of this term is required ensure that the matching + * documents are added to the set of required matches for this clause. + * + */ + if (clause.presence == lunr.Query.presence.REQUIRED) { + clauseMatches = clauseMatches.union(matchingDocumentsSet) + + if (requiredMatches[field] === undefined) { + requiredMatches[field] = lunr.Set.complete + } + } + + /* + * if the presence of this term is prohibited ensure that the matching + * documents are added to the set of prohibited matches for this field, + * creating that set if it does not yet exist. + */ + if (clause.presence == lunr.Query.presence.PROHIBITED) { + if (prohibitedMatches[field] === undefined) { + prohibitedMatches[field] = lunr.Set.empty + } + + prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet) + + /* + * Prohibited matches should not be part of the query vector used for + * similarity scoring and no metadata should be extracted so we continue + * to the next field + */ + continue + } + + /* + * The query field vector is populated using the termIndex found for + * the term and a unit value with the appropriate boost applied. + * Using upsert because there could already be an entry in the vector + * for the term we are working with. In that case we just add the scores + * together. + */ + queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b }) + + /** + * If we've already seen this term, field combo then we've already collected + * the matching documents and metadata, no need to go through all that again + */ + if (termFieldCache[termField]) { + continue + } + + for (var l = 0; l < matchingDocumentRefs.length; l++) { + /* + * All metadata for this term/field/document triple + * are then extracted and collected into an instance + * of lunr.MatchData ready to be returned in the query + * results + */ + var matchingDocumentRef = matchingDocumentRefs[l], + matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field), + metadata = fieldPosting[matchingDocumentRef], + fieldMatch + + if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) { + matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata) + } else { + fieldMatch.add(expandedTerm, field, metadata) + } + + } + + termFieldCache[termField] = true + } + } + } + + /** + * If the presence was required we need to update the requiredMatches field sets. + * We do this after all fields for the term have collected their matches because + * the clause terms presence is required in _any_ of the fields not _all_ of the + * fields. + */ + if (clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = requiredMatches[field].intersect(clauseMatches) + } + } + } + + /** + * Need to combine the field scoped required and prohibited + * matching documents into a global set of required and prohibited + * matches + */ + var allRequiredMatches = lunr.Set.complete, + allProhibitedMatches = lunr.Set.empty + + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i] + + if (requiredMatches[field]) { + allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field]) + } + + if (prohibitedMatches[field]) { + allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field]) + } + } + + var matchingFieldRefs = Object.keys(matchingFields), + results = [], + matches = Object.create(null) + + /* + * If the query is negated (contains only prohibited terms) + * we need to get _all_ fieldRefs currently existing in the + * index. This is only done when we know that the query is + * entirely prohibited terms to avoid any cost of getting all + * fieldRefs unnecessarily. + * + * Additionally, blank MatchData must be created to correctly + * populate the results. + */ + if (query.isNegated()) { + matchingFieldRefs = Object.keys(this.fieldVectors) + + for (var i = 0; i < matchingFieldRefs.length; i++) { + var matchingFieldRef = matchingFieldRefs[i] + var fieldRef = lunr.FieldRef.fromString(matchingFieldRef) + matchingFields[matchingFieldRef] = new lunr.MatchData + } + } + + for (var i = 0; i < matchingFieldRefs.length; i++) { + /* + * Currently we have document fields that match the query, but we + * need to return documents. The matchData and scores are combined + * from multiple fields belonging to the same document. + * + * Scores are calculated by field, using the query vectors created + * above, and combined into a final document score using addition. + */ + var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]), + docRef = fieldRef.docRef + + if (!allRequiredMatches.contains(docRef)) { + continue + } + + if (allProhibitedMatches.contains(docRef)) { + continue + } + + var fieldVector = this.fieldVectors[fieldRef], + score = queryVectors[fieldRef.fieldName].similarity(fieldVector), + docMatch + + if ((docMatch = matches[docRef]) !== undefined) { + docMatch.score += score + docMatch.matchData.combine(matchingFields[fieldRef]) + } else { + var match = { + ref: docRef, + score: score, + matchData: matchingFields[fieldRef] + } + matches[docRef] = match + results.push(match) + } + } + + /* + * Sort the results objects by score, highest first. + */ + return results.sort(function (a, b) { + return b.score - a.score + }) +} + +/** + * Prepares the index for JSON serialization. + * + * The schema for this JSON blob will be described in a + * separate JSON schema file. + * + * @returns {Object} + */ +lunr.Index.prototype.toJSON = function () { + var invertedIndex = Object.keys(this.invertedIndex) + .sort() + .map(function (term) { + return [term, this.invertedIndex[term]] + }, this) + + var fieldVectors = Object.keys(this.fieldVectors) + .map(function (ref) { + return [ref, this.fieldVectors[ref].toJSON()] + }, this) + + return { + version: lunr.version, + fields: this.fields, + fieldVectors: fieldVectors, + invertedIndex: invertedIndex, + pipeline: this.pipeline.toJSON() + } +} + +/** + * Loads a previously serialized lunr.Index + * + * @param {Object} serializedIndex - A previously serialized lunr.Index + * @returns {lunr.Index} + */ +lunr.Index.load = function (serializedIndex) { + var attrs = {}, + fieldVectors = {}, + serializedVectors = serializedIndex.fieldVectors, + invertedIndex = Object.create(null), + serializedInvertedIndex = serializedIndex.invertedIndex, + tokenSetBuilder = new lunr.TokenSet.Builder, + pipeline = lunr.Pipeline.load(serializedIndex.pipeline) + + if (serializedIndex.version != lunr.version) { + lunr.utils.warn("Version mismatch when loading serialised index. Current version of lunr '" + lunr.version + "' does not match serialized index '" + serializedIndex.version + "'") + } + + for (var i = 0; i < serializedVectors.length; i++) { + var tuple = serializedVectors[i], + ref = tuple[0], + elements = tuple[1] + + fieldVectors[ref] = new lunr.Vector(elements) + } + + for (var i = 0; i < serializedInvertedIndex.length; i++) { + var tuple = serializedInvertedIndex[i], + term = tuple[0], + posting = tuple[1] + + tokenSetBuilder.insert(term) + invertedIndex[term] = posting + } + + tokenSetBuilder.finish() + + attrs.fields = serializedIndex.fields + + attrs.fieldVectors = fieldVectors + attrs.invertedIndex = invertedIndex + attrs.tokenSet = tokenSetBuilder.root + attrs.pipeline = pipeline + + return new lunr.Index(attrs) +} +/*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Builder performs indexing on a set of documents and + * returns instances of lunr.Index ready for querying. + * + * All configuration of the index is done via the builder, the + * fields to index, the document reference, the text processing + * pipeline and document scoring parameters are all set on the + * builder before indexing. + * + * @constructor + * @property {string} _ref - Internal reference to the document reference field. + * @property {string[]} _fields - Internal reference to the document fields to index. + * @property {object} invertedIndex - The inverted index maps terms to document fields. + * @property {object} documentTermFrequencies - Keeps track of document term frequencies. + * @property {object} documentLengths - Keeps track of the length of documents added to the index. + * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing. + * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing. + * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index. + * @property {number} documentCount - Keeps track of the total number of documents indexed. + * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75. + * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2. + * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space. + * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index. + */ +lunr.Builder = function () { + this._ref = "id" + this._fields = Object.create(null) + this._documents = Object.create(null) + this.invertedIndex = Object.create(null) + this.fieldTermFrequencies = {} + this.fieldLengths = {} + this.tokenizer = lunr.tokenizer + this.pipeline = new lunr.Pipeline + this.searchPipeline = new lunr.Pipeline + this.documentCount = 0 + this._b = 0.75 + this._k1 = 1.2 + this.termIndex = 0 + this.metadataWhitelist = [] +} + +/** + * Sets the document field used as the document reference. Every document must have this field. + * The type of this field in the document should be a string, if it is not a string it will be + * coerced into a string by calling toString. + * + * The default ref is 'id'. + * + * The ref should _not_ be changed during indexing, it should be set before any documents are + * added to the index. Changing it during indexing can lead to inconsistent results. + * + * @param {string} ref - The name of the reference field in the document. + */ +lunr.Builder.prototype.ref = function (ref) { + this._ref = ref +} + +/** + * A function that is used to extract a field from a document. + * + * Lunr expects a field to be at the top level of a document, if however the field + * is deeply nested within a document an extractor function can be used to extract + * the right field for indexing. + * + * @callback fieldExtractor + * @param {object} doc - The document being added to the index. + * @returns {?(string|object|object[])} obj - The object that will be indexed for this field. + * @example Extracting a nested field + * function (doc) { return doc.nested.field } + */ + +/** + * Adds a field to the list of document fields that will be indexed. Every document being + * indexed should have this field. Null values for this field in indexed documents will + * not cause errors but will limit the chance of that document being retrieved by searches. + * + * All fields should be added before adding documents to the index. Adding fields after + * a document has been indexed will have no effect on already indexed documents. + * + * Fields can be boosted at build time. This allows terms within that field to have more + * importance when ranking search results. Use a field boost to specify that matches within + * one field are more important than other fields. + * + * @param {string} fieldName - The name of a field to index in all documents. + * @param {object} attributes - Optional attributes associated with this field. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this field. + * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document. + * @throws {RangeError} fieldName cannot contain unsupported characters '/' + */ +lunr.Builder.prototype.field = function (fieldName, attributes) { + if (/\//.test(fieldName)) { + throw new RangeError ("Field '" + fieldName + "' contains illegal character '/'") + } + + this._fields[fieldName] = attributes || {} +} + +/** + * A parameter to tune the amount of field length normalisation that is applied when + * calculating relevance scores. A value of 0 will completely disable any normalisation + * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b + * will be clamped to the range 0 - 1. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.b = function (number) { + if (number < 0) { + this._b = 0 + } else if (number > 1) { + this._b = 1 + } else { + this._b = number + } +} + +/** + * A parameter that controls the speed at which a rise in term frequency results in term + * frequency saturation. The default value is 1.2. Setting this to a higher value will give + * slower saturation levels, a lower value will result in quicker saturation. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.k1 = function (number) { + this._k1 = number +} + +/** + * Adds a document to the index. + * + * Before adding fields to the index the index should have been fully setup, with the document + * ref and all fields to index already having been specified. + * + * The document must have a field name as specified by the ref (by default this is 'id') and + * it should have all fields defined for indexing, though null or undefined values will not + * cause errors. + * + * Entire documents can be boosted at build time. Applying a boost to a document indicates that + * this document should rank higher in search results than other documents. + * + * @param {object} doc - The document to add to the index. + * @param {object} attributes - Optional attributes associated with this document. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this document. + */ +lunr.Builder.prototype.add = function (doc, attributes) { + var docRef = doc[this._ref], + fields = Object.keys(this._fields) + + this._documents[docRef] = attributes || {} + this.documentCount += 1 + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i], + extractor = this._fields[fieldName].extractor, + field = extractor ? extractor(doc) : doc[fieldName], + tokens = this.tokenizer(field, { + fields: [fieldName] + }), + terms = this.pipeline.run(tokens), + fieldRef = new lunr.FieldRef (docRef, fieldName), + fieldTerms = Object.create(null) + + this.fieldTermFrequencies[fieldRef] = fieldTerms + this.fieldLengths[fieldRef] = 0 + + // store the length of this field for this document + this.fieldLengths[fieldRef] += terms.length + + // calculate term frequencies for this field + for (var j = 0; j < terms.length; j++) { + var term = terms[j] + + if (fieldTerms[term] == undefined) { + fieldTerms[term] = 0 + } + + fieldTerms[term] += 1 + + // add to inverted index + // create an initial posting if one doesn't exist + if (this.invertedIndex[term] == undefined) { + var posting = Object.create(null) + posting["_index"] = this.termIndex + this.termIndex += 1 + + for (var k = 0; k < fields.length; k++) { + posting[fields[k]] = Object.create(null) + } + + this.invertedIndex[term] = posting + } + + // add an entry for this term/fieldName/docRef to the invertedIndex + if (this.invertedIndex[term][fieldName][docRef] == undefined) { + this.invertedIndex[term][fieldName][docRef] = Object.create(null) + } + + // store all whitelisted metadata about this token in the + // inverted index + for (var l = 0; l < this.metadataWhitelist.length; l++) { + var metadataKey = this.metadataWhitelist[l], + metadata = term.metadata[metadataKey] + + if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) { + this.invertedIndex[term][fieldName][docRef][metadataKey] = [] + } + + this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata) + } + } + + } +} + +/** + * Calculates the average document length for this index + * + * @private + */ +lunr.Builder.prototype.calculateAverageFieldLengths = function () { + + var fieldRefs = Object.keys(this.fieldLengths), + numberOfFields = fieldRefs.length, + accumulator = {}, + documentsWithField = {} + + for (var i = 0; i < numberOfFields; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + field = fieldRef.fieldName + + documentsWithField[field] || (documentsWithField[field] = 0) + documentsWithField[field] += 1 + + accumulator[field] || (accumulator[field] = 0) + accumulator[field] += this.fieldLengths[fieldRef] + } + + var fields = Object.keys(this._fields) + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i] + accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName] + } + + this.averageFieldLength = accumulator +} + +/** + * Builds a vector space model of every document using lunr.Vector + * + * @private + */ +lunr.Builder.prototype.createFieldVectors = function () { + var fieldVectors = {}, + fieldRefs = Object.keys(this.fieldTermFrequencies), + fieldRefsLength = fieldRefs.length, + termIdfCache = Object.create(null) + + for (var i = 0; i < fieldRefsLength; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + fieldName = fieldRef.fieldName, + fieldLength = this.fieldLengths[fieldRef], + fieldVector = new lunr.Vector, + termFrequencies = this.fieldTermFrequencies[fieldRef], + terms = Object.keys(termFrequencies), + termsLength = terms.length + + + var fieldBoost = this._fields[fieldName].boost || 1, + docBoost = this._documents[fieldRef.docRef].boost || 1 + + for (var j = 0; j < termsLength; j++) { + var term = terms[j], + tf = termFrequencies[term], + termIndex = this.invertedIndex[term]._index, + idf, score, scoreWithPrecision + + if (termIdfCache[term] === undefined) { + idf = lunr.idf(this.invertedIndex[term], this.documentCount) + termIdfCache[term] = idf + } else { + idf = termIdfCache[term] + } + + score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf) + score *= fieldBoost + score *= docBoost + scoreWithPrecision = Math.round(score * 1000) / 1000 + // Converts 1.23456789 to 1.234. + // Reducing the precision so that the vectors take up less + // space when serialised. Doing it now so that they behave + // the same before and after serialisation. Also, this is + // the fastest approach to reducing a number's precision in + // JavaScript. + + fieldVector.insert(termIndex, scoreWithPrecision) + } + + fieldVectors[fieldRef] = fieldVector + } + + this.fieldVectors = fieldVectors +} + +/** + * Creates a token set of all tokens in the index using lunr.TokenSet + * + * @private + */ +lunr.Builder.prototype.createTokenSet = function () { + this.tokenSet = lunr.TokenSet.fromArray( + Object.keys(this.invertedIndex).sort() + ) +} + +/** + * Builds the index, creating an instance of lunr.Index. + * + * This completes the indexing process and should only be called + * once all documents have been added to the index. + * + * @returns {lunr.Index} + */ +lunr.Builder.prototype.build = function () { + this.calculateAverageFieldLengths() + this.createFieldVectors() + this.createTokenSet() + + return new lunr.Index({ + invertedIndex: this.invertedIndex, + fieldVectors: this.fieldVectors, + tokenSet: this.tokenSet, + fields: Object.keys(this._fields), + pipeline: this.searchPipeline + }) +} + +/** + * Applies a plugin to the index builder. + * + * A plugin is a function that is called with the index builder as its context. + * Plugins can be used to customise or extend the behaviour of the index + * in some way. A plugin is just a function, that encapsulated the custom + * behaviour that should be applied when building the index. + * + * The plugin function will be called with the index builder as its argument, additional + * arguments can also be passed when calling use. The function will be called + * with the index builder as its context. + * + * @param {Function} plugin The plugin to apply. + */ +lunr.Builder.prototype.use = function (fn) { + var args = Array.prototype.slice.call(arguments, 1) + args.unshift(this) + fn.apply(this, args) +} +/** + * Contains and collects metadata about a matching document. + * A single instance of lunr.MatchData is returned as part of every + * lunr.Index~Result. + * + * @constructor + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + * @property {object} metadata - A cloned collection of metadata associated with this document. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData = function (term, field, metadata) { + var clonedMetadata = Object.create(null), + metadataKeys = Object.keys(metadata || {}) + + // Cloning the metadata to prevent the original + // being mutated during match data combination. + // Metadata is kept in an array within the inverted + // index so cloning the data can be done with + // Array#slice + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + clonedMetadata[key] = metadata[key].slice() + } + + this.metadata = Object.create(null) + + if (term !== undefined) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = clonedMetadata + } +} + +/** + * An instance of lunr.MatchData will be created for every term that matches a + * document. However only one instance is required in a lunr.Index~Result. This + * method combines metadata from another instance of lunr.MatchData with this + * objects metadata. + * + * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData.prototype.combine = function (otherMatchData) { + var terms = Object.keys(otherMatchData.metadata) + + for (var i = 0; i < terms.length; i++) { + var term = terms[i], + fields = Object.keys(otherMatchData.metadata[term]) + + if (this.metadata[term] == undefined) { + this.metadata[term] = Object.create(null) + } + + for (var j = 0; j < fields.length; j++) { + var field = fields[j], + keys = Object.keys(otherMatchData.metadata[term][field]) + + if (this.metadata[term][field] == undefined) { + this.metadata[term][field] = Object.create(null) + } + + for (var k = 0; k < keys.length; k++) { + var key = keys[k] + + if (this.metadata[term][field][key] == undefined) { + this.metadata[term][field][key] = otherMatchData.metadata[term][field][key] + } else { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key]) + } + + } + } + } +} + +/** + * Add metadata for a term/field pair to this instance of match data. + * + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + */ +lunr.MatchData.prototype.add = function (term, field, metadata) { + if (!(term in this.metadata)) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = metadata + return + } + + if (!(field in this.metadata[term])) { + this.metadata[term][field] = metadata + return + } + + var metadataKeys = Object.keys(metadata) + + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + + if (key in this.metadata[term][field]) { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key]) + } else { + this.metadata[term][field][key] = metadata[key] + } + } +} +/** + * A lunr.Query provides a programmatic way of defining queries to be performed + * against a {@link lunr.Index}. + * + * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method + * so the query object is pre-initialized with the right index fields. + * + * @constructor + * @property {lunr.Query~Clause[]} clauses - An array of query clauses. + * @property {string[]} allFields - An array of all available fields in a lunr.Index. + */ +lunr.Query = function (allFields) { + this.clauses = [] + this.allFields = allFields +} + +/** + * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause. + * + * This allows wildcards to be added to the beginning and end of a term without having to manually do any string + * concatenation. + * + * The wildcard constants can be bitwise combined to select both leading and trailing wildcards. + * + * @constant + * @default + * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour + * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists + * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with trailing wildcard + * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING }) + * @example query term with leading and trailing wildcard + * query.term('foo', { + * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING + * }) + */ + +lunr.Query.wildcard = new String ("*") +lunr.Query.wildcard.NONE = 0 +lunr.Query.wildcard.LEADING = 1 +lunr.Query.wildcard.TRAILING = 2 + +/** + * Constants for indicating what kind of presence a term must have in matching documents. + * + * @constant + * @enum {number} + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with required presence + * query.term('foo', { presence: lunr.Query.presence.REQUIRED }) + */ +lunr.Query.presence = { + /** + * Term's presence in a document is optional, this is the default value. + */ + OPTIONAL: 1, + + /** + * Term's presence in a document is required, documents that do not contain + * this term will not be returned. + */ + REQUIRED: 2, + + /** + * Term's presence in a document is prohibited, documents that do contain + * this term will not be returned. + */ + PROHIBITED: 3 +} + +/** + * A single clause in a {@link lunr.Query} contains a term and details on how to + * match that term against a {@link lunr.Index}. + * + * @typedef {Object} lunr.Query~Clause + * @property {string[]} fields - The fields in an index this clause should be matched against. + * @property {number} [boost=1] - Any boost that should be applied when matching this clause. + * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be. + * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline. + * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended. + * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents. + */ + +/** + * Adds a {@link lunr.Query~Clause} to this query. + * + * Unless the clause contains the fields to be matched all fields will be matched. In addition + * a default boost of 1 is applied to the clause. + * + * @param {lunr.Query~Clause} clause - The clause to add to this query. + * @see lunr.Query~Clause + * @returns {lunr.Query} + */ +lunr.Query.prototype.clause = function (clause) { + if (!('fields' in clause)) { + clause.fields = this.allFields + } + + if (!('boost' in clause)) { + clause.boost = 1 + } + + if (!('usePipeline' in clause)) { + clause.usePipeline = true + } + + if (!('wildcard' in clause)) { + clause.wildcard = lunr.Query.wildcard.NONE + } + + if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) { + clause.term = "*" + clause.term + } + + if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) { + clause.term = "" + clause.term + "*" + } + + if (!('presence' in clause)) { + clause.presence = lunr.Query.presence.OPTIONAL + } + + this.clauses.push(clause) + + return this +} + +/** + * A negated query is one in which every clause has a presence of + * prohibited. These queries require some special processing to return + * the expected results. + * + * @returns boolean + */ +lunr.Query.prototype.isNegated = function () { + for (var i = 0; i < this.clauses.length; i++) { + if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) { + return false + } + } + + return true +} + +/** + * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause} + * to the list of clauses that make up this query. + * + * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion + * to a token or token-like string should be done before calling this method. + * + * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an + * array, each term in the array will share the same options. + * + * @param {object|object[]} term - The term(s) to add to the query. + * @param {object} [options] - Any additional properties to add to the query clause. + * @returns {lunr.Query} + * @see lunr.Query#clause + * @see lunr.Query~Clause + * @example adding a single term to a query + * query.term("foo") + * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard + * query.term("foo", { + * fields: ["title"], + * boost: 10, + * wildcard: lunr.Query.wildcard.TRAILING + * }) + * @example using lunr.tokenizer to convert a string to tokens before using them as terms + * query.term(lunr.tokenizer("foo bar")) + */ +lunr.Query.prototype.term = function (term, options) { + if (Array.isArray(term)) { + term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this) + return this + } + + var clause = options || {} + clause.term = term.toString() + + this.clause(clause) + + return this +} +lunr.QueryParseError = function (message, start, end) { + this.name = "QueryParseError" + this.message = message + this.start = start + this.end = end +} + +lunr.QueryParseError.prototype = new Error +lunr.QueryLexer = function (str) { + this.lexemes = [] + this.str = str + this.length = str.length + this.pos = 0 + this.start = 0 + this.escapeCharPositions = [] +} + +lunr.QueryLexer.prototype.run = function () { + var state = lunr.QueryLexer.lexText + + while (state) { + state = state(this) + } +} + +lunr.QueryLexer.prototype.sliceString = function () { + var subSlices = [], + sliceStart = this.start, + sliceEnd = this.pos + + for (var i = 0; i < this.escapeCharPositions.length; i++) { + sliceEnd = this.escapeCharPositions[i] + subSlices.push(this.str.slice(sliceStart, sliceEnd)) + sliceStart = sliceEnd + 1 + } + + subSlices.push(this.str.slice(sliceStart, this.pos)) + this.escapeCharPositions.length = 0 + + return subSlices.join('') +} + +lunr.QueryLexer.prototype.emit = function (type) { + this.lexemes.push({ + type: type, + str: this.sliceString(), + start: this.start, + end: this.pos + }) + + this.start = this.pos +} + +lunr.QueryLexer.prototype.escapeCharacter = function () { + this.escapeCharPositions.push(this.pos - 1) + this.pos += 1 +} + +lunr.QueryLexer.prototype.next = function () { + if (this.pos >= this.length) { + return lunr.QueryLexer.EOS + } + + var char = this.str.charAt(this.pos) + this.pos += 1 + return char +} + +lunr.QueryLexer.prototype.width = function () { + return this.pos - this.start +} + +lunr.QueryLexer.prototype.ignore = function () { + if (this.start == this.pos) { + this.pos += 1 + } + + this.start = this.pos +} + +lunr.QueryLexer.prototype.backup = function () { + this.pos -= 1 +} + +lunr.QueryLexer.prototype.acceptDigitRun = function () { + var char, charCode + + do { + char = this.next() + charCode = char.charCodeAt(0) + } while (charCode > 47 && charCode < 58) + + if (char != lunr.QueryLexer.EOS) { + this.backup() + } +} + +lunr.QueryLexer.prototype.more = function () { + return this.pos < this.length +} + +lunr.QueryLexer.EOS = 'EOS' +lunr.QueryLexer.FIELD = 'FIELD' +lunr.QueryLexer.TERM = 'TERM' +lunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE' +lunr.QueryLexer.BOOST = 'BOOST' +lunr.QueryLexer.PRESENCE = 'PRESENCE' + +lunr.QueryLexer.lexField = function (lexer) { + lexer.backup() + lexer.emit(lunr.QueryLexer.FIELD) + lexer.ignore() + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexTerm = function (lexer) { + if (lexer.width() > 1) { + lexer.backup() + lexer.emit(lunr.QueryLexer.TERM) + } + + lexer.ignore() + + if (lexer.more()) { + return lunr.QueryLexer.lexText + } +} + +lunr.QueryLexer.lexEditDistance = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.EDIT_DISTANCE) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexBoost = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.BOOST) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexEOS = function (lexer) { + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } +} + +// This matches the separator used when tokenising fields +// within a document. These should match otherwise it is +// not possible to search for some tokens within a document. +// +// It is possible for the user to change the separator on the +// tokenizer so it _might_ clash with any other of the special +// characters already used within the search string, e.g. :. +// +// This means that it is possible to change the separator in +// such a way that makes some words unsearchable using a search +// string. +lunr.QueryLexer.termSeparator = lunr.tokenizer.separator + +lunr.QueryLexer.lexText = function (lexer) { + while (true) { + var char = lexer.next() + + if (char == lunr.QueryLexer.EOS) { + return lunr.QueryLexer.lexEOS + } + + // Escape character is '\' + if (char.charCodeAt(0) == 92) { + lexer.escapeCharacter() + continue + } + + if (char == ":") { + return lunr.QueryLexer.lexField + } + + if (char == "~") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexEditDistance + } + + if (char == "^") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexBoost + } + + // "+" indicates term presence is required + // checking for length to ensure that only + // leading "+" are considered + if (char == "+" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + // "-" indicates term presence is prohibited + // checking for length to ensure that only + // leading "-" are considered + if (char == "-" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + if (char.match(lunr.QueryLexer.termSeparator)) { + return lunr.QueryLexer.lexTerm + } + } +} + +lunr.QueryParser = function (str, query) { + this.lexer = new lunr.QueryLexer (str) + this.query = query + this.currentClause = {} + this.lexemeIdx = 0 +} + +lunr.QueryParser.prototype.parse = function () { + this.lexer.run() + this.lexemes = this.lexer.lexemes + + var state = lunr.QueryParser.parseClause + + while (state) { + state = state(this) + } + + return this.query +} + +lunr.QueryParser.prototype.peekLexeme = function () { + return this.lexemes[this.lexemeIdx] +} + +lunr.QueryParser.prototype.consumeLexeme = function () { + var lexeme = this.peekLexeme() + this.lexemeIdx += 1 + return lexeme +} + +lunr.QueryParser.prototype.nextClause = function () { + var completedClause = this.currentClause + this.query.clause(completedClause) + this.currentClause = {} +} + +lunr.QueryParser.parseClause = function (parser) { + var lexeme = parser.peekLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.type) { + case lunr.QueryLexer.PRESENCE: + return lunr.QueryParser.parsePresence + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expected either a field or a term, found " + lexeme.type + + if (lexeme.str.length >= 1) { + errorMessage += " with value '" + lexeme.str + "'" + } + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } +} + +lunr.QueryParser.parsePresence = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.str) { + case "-": + parser.currentClause.presence = lunr.Query.presence.PROHIBITED + break + case "+": + parser.currentClause.presence = lunr.Query.presence.REQUIRED + break + default: + var errorMessage = "unrecognised presence operator'" + lexeme.str + "'" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term or field, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term or field, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseField = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + if (parser.query.allFields.indexOf(lexeme.str) == -1) { + var possibleFields = parser.query.allFields.map(function (f) { return "'" + f + "'" }).join(', '), + errorMessage = "unrecognised field '" + lexeme.str + "', possible fields: " + possibleFields + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.fields = [lexeme.str] + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseTerm = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + parser.currentClause.term = lexeme.str.toLowerCase() + + if (lexeme.str.indexOf("*") != -1) { + parser.currentClause.usePipeline = false + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseEditDistance = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var editDistance = parseInt(lexeme.str, 10) + + if (isNaN(editDistance)) { + var errorMessage = "edit distance must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.editDistance = editDistance + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseBoost = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var boost = parseInt(lexeme.str, 10) + + if (isNaN(boost)) { + var errorMessage = "boost must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.boost = boost + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + + /** + * export the module via AMD, CommonJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js + */ + ;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like environments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + root.lunr = factory() + } + }(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + return lunr + })) +})(); diff --git a/search/main.js b/search/main.js new file mode 100644 index 0000000..a5e469d --- /dev/null +++ b/search/main.js @@ -0,0 +1,109 @@ +function getSearchTermFromLocation() { + var sPageURL = window.location.search.substring(1); + var sURLVariables = sPageURL.split('&'); + for (var i = 0; i < sURLVariables.length; i++) { + var sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] == 'q') { + return decodeURIComponent(sParameterName[1].replace(/\+/g, '%20')); + } + } +} + +function joinUrl (base, path) { + if (path.substring(0, 1) === "/") { + // path starts with `/`. Thus it is absolute. + return path; + } + if (base.substring(base.length-1) === "/") { + // base ends with `/` + return base + path; + } + return base + "/" + path; +} + +function escapeHtml (value) { + return value.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +} + +function formatResult (location, title, summary) { + return ''; +} + +function displayResults (results) { + var search_results = document.getElementById("mkdocs-search-results"); + while (search_results.firstChild) { + search_results.removeChild(search_results.firstChild); + } + if (results.length > 0){ + for (var i=0; i < results.length; i++){ + var result = results[i]; + var html = formatResult(result.location, result.title, result.summary); + search_results.insertAdjacentHTML('beforeend', html); + } + } else { + var noResultsText = search_results.getAttribute('data-no-results-text'); + if (!noResultsText) { + noResultsText = "No results found"; + } + search_results.insertAdjacentHTML('beforeend', '

' + noResultsText + '

'); + } +} + +function doSearch () { + var query = document.getElementById('mkdocs-search-query').value; + if (query.length > min_search_length) { + if (!window.Worker) { + displayResults(search(query)); + } else { + searchWorker.postMessage({query: query}); + } + } else { + // Clear results for short queries + displayResults([]); + } +} + +function initSearch () { + var search_input = document.getElementById('mkdocs-search-query'); + if (search_input) { + search_input.addEventListener("keyup", doSearch); + } + var term = getSearchTermFromLocation(); + if (term) { + search_input.value = term; + doSearch(); + } +} + +function onWorkerMessage (e) { + if (e.data.allowSearch) { + initSearch(); + } else if (e.data.results) { + var results = e.data.results; + displayResults(results); + } else if (e.data.config) { + min_search_length = e.data.config.min_search_length-1; + } +} + +if (!window.Worker) { + console.log('Web Worker API not supported'); + // load index in main thread + $.getScript(joinUrl(base_url, "search/worker.js")).done(function () { + console.log('Loaded worker'); + init(); + window.postMessage = function (msg) { + onWorkerMessage({data: msg}); + }; + }).fail(function (jqxhr, settings, exception) { + console.error('Could not load worker.js'); + }); +} else { + // Wrap search in a web worker + var searchWorker = new Worker(joinUrl(base_url, "search/worker.js")); + searchWorker.postMessage({init: true}); + searchWorker.onmessage = onWorkerMessage; +} diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 0000000..79e8a76 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"Seattle Community Network Docs Welcome to the documentation website for the Seattle Community Network ! If you're looking for our main website, it is located at https://www.seattlecommunitynetwork.org . You're in the Right Place Seattle Community Network has a place for everyone . Whether you'd like to join to get free Internet , get involved to help out your community, learn some skills so that you can get a job, or all of the above! PRO TIP We are a community . It's in our name! So, why not start by joining our community ? It's easy. What's here? Some topics you can find on this website include: FAQ - get the answers to some common questions. Community - get involved and learn more about our community, our rules, and what we're up to. Learn - gain some new skills that you can use to help out with our networks. Infrastructure - get the details on how our networks work behind the scenes.","title":"Home"},{"location":"#seattle-community-network-docs","text":"Welcome to the documentation website for the Seattle Community Network ! If you're looking for our main website, it is located at https://www.seattlecommunitynetwork.org .","title":"Seattle Community Network Docs"},{"location":"#youre-in-the-right-place","text":"Seattle Community Network has a place for everyone . Whether you'd like to join to get free Internet , get involved to help out your community, learn some skills so that you can get a job, or all of the above! PRO TIP We are a community . It's in our name! So, why not start by joining our community ? It's easy.","title":"You're in the Right Place"},{"location":"#whats-here","text":"Some topics you can find on this website include: FAQ - get the answers to some common questions. Community - get involved and learn more about our community, our rules, and what we're up to. Learn - gain some new skills that you can use to help out with our networks. Infrastructure - get the details on how our networks work behind the scenes.","title":"What's here?"},{"location":"community/code-of-conduct/","text":"Code of Conduct The Seattle Community Network (SCN) is dedicated to fostering an environment in which everyone can participate in our meetups, installs, online spaces, and any other community event. We believe that diversity in our community is critical and should be celebrated. We welcome everyone of any race, age, gender, nationality, gender identity and expression, sexual orientation, disability, physical appearance, body size, religion, education, and skill level. The SCN community and experience often extends outside those spaces. Members meet in person to collaborate on projects, attend related meetups or conferences together, and communicate on social media. Abusive or unwelcoming behavior between SCN Members still has a profound impact on individuals and on the community when it happens beyond our events and online forums. We will use our discretion when deciding whether to enforce this code of conduct after reports of such behavior happening outside of our spaces, taking into account the impact on the individual Members involved as well as the impact on the community at large. Types of Behavior Encouraged Behavior All participants are expected to: be considerate, respectful, and collaborative. Specifically we expect participants to: Demonstrate empathy, kindness, and patience toward other people Assume the best intentions from others Be respectful of differing opinions, viewpoints, and experiences Give and gracefully accept constructive feedback Accept responsibility and apologize to those affected by our mistakes, and learn from the experience Focus on what is best not just for us as individuals, but for the overall community Unacceptable Behavior The following types of behavior are unacceptable at SCN, both online and in-person, and constitute code of conduct violations. Abusive Behavior Harassment Offensive verbal comments related to gender, sexual orientation, disability, physical appearance, body size, race, nationality, immigration status, language, religion, or education level Sexual images in public spaces, unwelcome sexual or romantic attention, and inappropriate physical contact. Threats Threating someone physically or verbally. For example, threatening to publicize sensitive information about someone's personal life Hacking Any kind of malicious or harmful behavior towards other network users and their devices/data/property, or towards the network and its equipment or normal functioning. For example, DDOS attacks or unauthorized remote access. Unwelcoming Behavior Blatant -isms Saying things that are explicitly racist, sexist, homophobic, etc. For example, arguing that some people are less intelligent because of their gender, race or religion. Subtle -isms and small mistakes made in conversation are not code of conduct violations. However, repeating something after it has been pointed out to you that you made a member feel unwelcome, broke a social rule or antagonizing or arguing with someone who has pointed out your subtle -ism is considered unwelcoming behavior and is not allowed in SCN. Maliciousness towards other members Deliberately attempting to make others feel bad, name-calling, singling out others for derision or exclusion. For example, telling somone they're not technical enough or that they don't belong in SCN. Being especially unpleasant For example, if we've received reports from multiple members of annoying, rude, or especially distracting behavior. For example, repeatedly engaging in bad faith arguments, talking down to people, or excluding people from participation. Reporting Please report code of conduct violations either to the event organizer, by emailing lcl@seattlecommunitynetwork.org or by alerting moderators on Discord with the @moderators group tag. All of our moderators are volunteers, and will response with their best effort. However, if you provide your name, or contact info we promise to respond within two business days. How to Report In your report, please include: Subject line \"[SCN Code of Conduct Violation Report]\" followed by descriptive subject Your name This is incredibly helpful for us to be able to follow up with you, and ask questions to better understand the situation. You are welcome to report anonymously. Please only use this option if you really need to, and know that we might not be able to take action without knowing who you are. In any case, provide an email address so we can correspond with you about the report. A detailed description of what happened If the violation happened online, please link to or send us the relevant text. If the violation happened in person, please detail exactly what the other person said or did. In order to take action, we need to know the concrete actions that someone took. Where and when the incident happened Any other relevant context. Do you have examples of a pattern of similar behavior? Do you have a relationship with this person outside of SCN? If/how you've already responded - this lets us know the current state of the situation. Why to Report You are responsible for making SCN a safe and comfortable space for everyone. Everyone in our community shares this responsibility. Our volunteer Moderators are not around all the time, so we cannot enforce the code of conduct without your help. The consequences for SCN of not reporting bad behavior outweigh the consequences for one person reporting it. We sometimes hear \u201cI don\u2019t want X person to meet consequences because I told someone about their bad behavior.\u201d Consider the impact on everyone else at SCN of letting their behavior continue unchecked. SCN only works as a self-directed, community-driven effort because of shared trust between members. Reporting code of conduct violations helps us identify when this trust is broken, to prevent that from happening in the future. Enforcement Guidelines Moderators will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct. Correction Community Impact: Unwelcoming behavior. Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. Consequence: A private, written warning from moderators, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public or private apology may be requested in the venue where the behavior took place, for example on Discord or at the event. Warning Community Impact: A violation through a single incident of abusive behavior or series of unwelcoming behaviors. Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. Temporary Ban Community Impact: A serious violation of community standards, including sustained unwelcoming behavior. Consequence: A temporary ban from any sort of participation, interaction, or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. Permanent Ban Community Impact: Demonstrating a pattern of violation of community standards, including sustained unwelcoming behavior, harassment or threatening of an individual, or aggression toward or disparagement of classes of individuals. Consequence: A permanent ban from any sort of participation, or public interaction within the community. Discussion If you have questions about any aspect of the code of conduct, or want to propose amending it, please contact the @moderators group, or drop in to the public #governance channel on Discord. Attribution Taken almost verbatim from the NYC Mesh Code of Conduct . NYC Mesh CoC Attribution: Most of this is from the Recurse Center CoC. Other parts are from Strange Loop(community goals), Contributor Covenant (portion of the community goals, encouraged behaviors, enforcement guidelines), and the Toronto Mesh/Geek Feminism (guidelines for moderators).","title":"Code of Conduct"},{"location":"community/code-of-conduct/#code-of-conduct","text":"The Seattle Community Network (SCN) is dedicated to fostering an environment in which everyone can participate in our meetups, installs, online spaces, and any other community event. We believe that diversity in our community is critical and should be celebrated. We welcome everyone of any race, age, gender, nationality, gender identity and expression, sexual orientation, disability, physical appearance, body size, religion, education, and skill level. The SCN community and experience often extends outside those spaces. Members meet in person to collaborate on projects, attend related meetups or conferences together, and communicate on social media. Abusive or unwelcoming behavior between SCN Members still has a profound impact on individuals and on the community when it happens beyond our events and online forums. We will use our discretion when deciding whether to enforce this code of conduct after reports of such behavior happening outside of our spaces, taking into account the impact on the individual Members involved as well as the impact on the community at large.","title":"Code of Conduct"},{"location":"community/code-of-conduct/#types-of-behavior","text":"","title":"Types of Behavior"},{"location":"community/code-of-conduct/#encouraged-behavior","text":"All participants are expected to: be considerate, respectful, and collaborative. Specifically we expect participants to: Demonstrate empathy, kindness, and patience toward other people Assume the best intentions from others Be respectful of differing opinions, viewpoints, and experiences Give and gracefully accept constructive feedback Accept responsibility and apologize to those affected by our mistakes, and learn from the experience Focus on what is best not just for us as individuals, but for the overall community","title":"Encouraged Behavior"},{"location":"community/code-of-conduct/#unacceptable-behavior","text":"The following types of behavior are unacceptable at SCN, both online and in-person, and constitute code of conduct violations.","title":"Unacceptable Behavior"},{"location":"community/code-of-conduct/#abusive-behavior","text":"Harassment Offensive verbal comments related to gender, sexual orientation, disability, physical appearance, body size, race, nationality, immigration status, language, religion, or education level Sexual images in public spaces, unwelcome sexual or romantic attention, and inappropriate physical contact. Threats Threating someone physically or verbally. For example, threatening to publicize sensitive information about someone's personal life Hacking Any kind of malicious or harmful behavior towards other network users and their devices/data/property, or towards the network and its equipment or normal functioning. For example, DDOS attacks or unauthorized remote access.","title":"Abusive Behavior"},{"location":"community/code-of-conduct/#unwelcoming-behavior","text":"Blatant -isms Saying things that are explicitly racist, sexist, homophobic, etc. For example, arguing that some people are less intelligent because of their gender, race or religion. Subtle -isms and small mistakes made in conversation are not code of conduct violations. However, repeating something after it has been pointed out to you that you made a member feel unwelcome, broke a social rule or antagonizing or arguing with someone who has pointed out your subtle -ism is considered unwelcoming behavior and is not allowed in SCN. Maliciousness towards other members Deliberately attempting to make others feel bad, name-calling, singling out others for derision or exclusion. For example, telling somone they're not technical enough or that they don't belong in SCN. Being especially unpleasant For example, if we've received reports from multiple members of annoying, rude, or especially distracting behavior. For example, repeatedly engaging in bad faith arguments, talking down to people, or excluding people from participation.","title":"Unwelcoming Behavior"},{"location":"community/code-of-conduct/#reporting","text":"Please report code of conduct violations either to the event organizer, by emailing lcl@seattlecommunitynetwork.org or by alerting moderators on Discord with the @moderators group tag. All of our moderators are volunteers, and will response with their best effort. However, if you provide your name, or contact info we promise to respond within two business days.","title":"Reporting"},{"location":"community/code-of-conduct/#how-to-report","text":"In your report, please include: Subject line \"[SCN Code of Conduct Violation Report]\" followed by descriptive subject Your name This is incredibly helpful for us to be able to follow up with you, and ask questions to better understand the situation. You are welcome to report anonymously. Please only use this option if you really need to, and know that we might not be able to take action without knowing who you are. In any case, provide an email address so we can correspond with you about the report. A detailed description of what happened If the violation happened online, please link to or send us the relevant text. If the violation happened in person, please detail exactly what the other person said or did. In order to take action, we need to know the concrete actions that someone took. Where and when the incident happened Any other relevant context. Do you have examples of a pattern of similar behavior? Do you have a relationship with this person outside of SCN? If/how you've already responded - this lets us know the current state of the situation.","title":"How to Report"},{"location":"community/code-of-conduct/#why-to-report","text":"You are responsible for making SCN a safe and comfortable space for everyone. Everyone in our community shares this responsibility. Our volunteer Moderators are not around all the time, so we cannot enforce the code of conduct without your help. The consequences for SCN of not reporting bad behavior outweigh the consequences for one person reporting it. We sometimes hear \u201cI don\u2019t want X person to meet consequences because I told someone about their bad behavior.\u201d Consider the impact on everyone else at SCN of letting their behavior continue unchecked. SCN only works as a self-directed, community-driven effort because of shared trust between members. Reporting code of conduct violations helps us identify when this trust is broken, to prevent that from happening in the future.","title":"Why to Report"},{"location":"community/code-of-conduct/#enforcement-guidelines","text":"Moderators will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct.","title":"Enforcement Guidelines"},{"location":"community/code-of-conduct/#correction","text":"Community Impact: Unwelcoming behavior. Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. Consequence: A private, written warning from moderators, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public or private apology may be requested in the venue where the behavior took place, for example on Discord or at the event.","title":"Correction"},{"location":"community/code-of-conduct/#warning","text":"Community Impact: A violation through a single incident of abusive behavior or series of unwelcoming behaviors. Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.","title":"Warning"},{"location":"community/code-of-conduct/#temporary-ban","text":"Community Impact: A serious violation of community standards, including sustained unwelcoming behavior. Consequence: A temporary ban from any sort of participation, interaction, or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.","title":"Temporary Ban"},{"location":"community/code-of-conduct/#permanent-ban","text":"Community Impact: Demonstrating a pattern of violation of community standards, including sustained unwelcoming behavior, harassment or threatening of an individual, or aggression toward or disparagement of classes of individuals. Consequence: A permanent ban from any sort of participation, or public interaction within the community.","title":"Permanent Ban"},{"location":"community/code-of-conduct/#discussion","text":"If you have questions about any aspect of the code of conduct, or want to propose amending it, please contact the @moderators group, or drop in to the public #governance channel on Discord.","title":"Discussion"},{"location":"community/code-of-conduct/#attribution","text":"Taken almost verbatim from the NYC Mesh Code of Conduct . NYC Mesh CoC Attribution: Most of this is from the Recurse Center CoC. Other parts are from Strange Loop(community goals), Contributor Covenant (portion of the community goals, encouraged behaviors, enforcement guidelines), and the Toronto Mesh/Geek Feminism (guidelines for moderators).","title":"Attribution"},{"location":"community/community-networking-toolkit/","text":"Community Networking Toolkit This is a toolkit/resource guide for anyone interested in starting up their own community network. Recommended Deployment Kit Here is a spreadsheet containing a bunch of tools and equipment that you should bring to network deployments. This was created by Esther Jang who has a lot of practical experience deploying sites. This spreadsheet is color-coded into different categories based on how important they are to have! Ethernet Crimping Ethernet crimping is an important skill for network site installations. The length of an ethernet cable connection is only truly known once you're on site so it is useful to be able to quickly cut ethernet cable to the desired length and crimp it. Our Guide to Crimping Workshop Slides Crimping Video Cable Testing Video (first 7 min only for basics) Ethernet Color Coding Diagrams Both the T-568A and the T-568B standard Straight-Through cables are used most often as patch cords for your Ethernet connections. If you require a cable to connect two Ethernet devices directly together without a hub or when you connect two hubs together, you will need to use a Crossover cable instead. Internet Society (ISOC) Materials Introduction to Community Networks All-aspects Community Networking Guides AlterMundi (Spanish Language) NYC Mesh - How to start a community network NYC Mesh Docs Telecommunications Reclaimed - Linked from NYC Mesh guide Start your own ISP This site is dedicated to helping you start your own Internet Service Provider. Specifically this guide is about building a Wireless ISP (WISP). Commotion Construction Kit The Commotion Construction Kit is a set of documentation tools that the Open Technology Institute has used in workshops around the world and at home. It is a \u201cdo it ourselves\u201d guide to building community wireless networks. Neighborhood Network Construction Kit The Neighborhood Network Construction Kit is a set of documentation tools that the Open Technology Institute has used in workshops around the world and at home. It is a \u201cdo it ourselves\u201d guide to building community wireless networks. Many of these activities were first released as part of the Commotion project. Community Technology Field Guide A collective resource for digital stewardship, digital justice and community infrastructure. These resources emphasize self-governance, participatory learning, collaborative design and sustainability. Building Broadband Commons - Tools for Planners and Communities Next Century City - Becoming Broadband Ready, a Toolkit for Communities Wireless Networking in the Developing World - Online Book Wireless Networking in the Developing World is a free book about designing, implementing, and maintaining low-cost wireless networks. Community Networks in Comics It is not easy to explain the concepts behind community networks, both the technical characteristics of radio frequency networks and the social and human aspects of community technologies. Teaching a workshop for popular groups using colonizing terms and methodologies can increase the existing barrier between people and a technology that was not created for their interests. With this in mind, images and analogies are powerful tools to make it easier to explain a technical term or an idea. We reject the premise that to do so would in any way underestimate people\u2019s ability to understand technical matters. Building Consentful Tech Zine In 2017, Una Lee, Dann Toliver, and their design firm And Also Too published the Building Consentful Tech Zine as a provocation on how to think about and design for consentfulness. This framing really resonated with our group, so we expanded it into a project where we prototype what this looks like in practice (and learned some interaction design methodologies on the way)! Report on Digital Skill Sets for Diverse Users The City of Seattle, in partnership with Technology and Social Change Group (TASCHA), developed a set of Digital Equity Indicators that helps measure Seattle\u2019s progress in meeting the initiative's goals. What digital skills do existing frameworks and curricula cover? What digital skills should the City and partners recommend to digital skill instructors to teach and promote? Do these resources have corresponding assessments to help assess individuals\u2019 digital skill abilities? Worksheet for Digital Skills for Diverse Users A list of 74 skills identified by TASCHA (see above resource) to include in digital equity curriculums Report on current state of Detroit Community Network In this case study, we focus on Detroit and the predominantly Black and lower-income neighborhood of the North End as an example of innovative, community-scale projects that are locally generated. New Community Networks by Douglas Schuler Online book about building and socially sustaining community networks, based on Doug Schuler's experiences with the Seattle Community Network project in the 90's (1996)","title":"Community Networking Toolkit"},{"location":"community/community-networking-toolkit/#community-networking-toolkit","text":"This is a toolkit/resource guide for anyone interested in starting up their own community network.","title":"Community Networking Toolkit"},{"location":"community/community-networking-toolkit/#recommended-deployment-kit","text":"Here is a spreadsheet containing a bunch of tools and equipment that you should bring to network deployments. This was created by Esther Jang who has a lot of practical experience deploying sites. This spreadsheet is color-coded into different categories based on how important they are to have!","title":"Recommended Deployment Kit"},{"location":"community/community-networking-toolkit/#ethernet-crimping","text":"Ethernet crimping is an important skill for network site installations. The length of an ethernet cable connection is only truly known once you're on site so it is useful to be able to quickly cut ethernet cable to the desired length and crimp it. Our Guide to Crimping Workshop Slides Crimping Video Cable Testing Video (first 7 min only for basics) Ethernet Color Coding Diagrams Both the T-568A and the T-568B standard Straight-Through cables are used most often as patch cords for your Ethernet connections. If you require a cable to connect two Ethernet devices directly together without a hub or when you connect two hubs together, you will need to use a Crossover cable instead.","title":"Ethernet Crimping"},{"location":"community/community-networking-toolkit/#internet-society-isoc-materials","text":"Introduction to Community Networks","title":"Internet Society (ISOC) Materials"},{"location":"community/community-networking-toolkit/#all-aspects-community-networking-guides","text":"AlterMundi (Spanish Language) NYC Mesh - How to start a community network NYC Mesh Docs Telecommunications Reclaimed - Linked from NYC Mesh guide Start your own ISP This site is dedicated to helping you start your own Internet Service Provider. Specifically this guide is about building a Wireless ISP (WISP). Commotion Construction Kit The Commotion Construction Kit is a set of documentation tools that the Open Technology Institute has used in workshops around the world and at home. It is a \u201cdo it ourselves\u201d guide to building community wireless networks. Neighborhood Network Construction Kit The Neighborhood Network Construction Kit is a set of documentation tools that the Open Technology Institute has used in workshops around the world and at home. It is a \u201cdo it ourselves\u201d guide to building community wireless networks. Many of these activities were first released as part of the Commotion project. Community Technology Field Guide A collective resource for digital stewardship, digital justice and community infrastructure. These resources emphasize self-governance, participatory learning, collaborative design and sustainability. Building Broadband Commons - Tools for Planners and Communities Next Century City - Becoming Broadband Ready, a Toolkit for Communities Wireless Networking in the Developing World - Online Book Wireless Networking in the Developing World is a free book about designing, implementing, and maintaining low-cost wireless networks. Community Networks in Comics It is not easy to explain the concepts behind community networks, both the technical characteristics of radio frequency networks and the social and human aspects of community technologies. Teaching a workshop for popular groups using colonizing terms and methodologies can increase the existing barrier between people and a technology that was not created for their interests. With this in mind, images and analogies are powerful tools to make it easier to explain a technical term or an idea. We reject the premise that to do so would in any way underestimate people\u2019s ability to understand technical matters. Building Consentful Tech Zine In 2017, Una Lee, Dann Toliver, and their design firm And Also Too published the Building Consentful Tech Zine as a provocation on how to think about and design for consentfulness. This framing really resonated with our group, so we expanded it into a project where we prototype what this looks like in practice (and learned some interaction design methodologies on the way)! Report on Digital Skill Sets for Diverse Users The City of Seattle, in partnership with Technology and Social Change Group (TASCHA), developed a set of Digital Equity Indicators that helps measure Seattle\u2019s progress in meeting the initiative's goals. What digital skills do existing frameworks and curricula cover? What digital skills should the City and partners recommend to digital skill instructors to teach and promote? Do these resources have corresponding assessments to help assess individuals\u2019 digital skill abilities? Worksheet for Digital Skills for Diverse Users A list of 74 skills identified by TASCHA (see above resource) to include in digital equity curriculums Report on current state of Detroit Community Network In this case study, we focus on Detroit and the predominantly Black and lower-income neighborhood of the North End as an example of innovative, community-scale projects that are locally generated. New Community Networks by Douglas Schuler Online book about building and socially sustaining community networks, based on Doug Schuler's experiences with the Seattle Community Network project in the 90's (1996)","title":"All-aspects Community Networking Guides"},{"location":"community/join/","text":"Join Us Seattle Community Network has a place for everyone . Whether you'd like to join to get free Internet, get involved to help out your community, learn some skills so that you can get a job, or all of the above! PRO TIP Do everything on this page! Our community is spread out in a lot of different places, so doing everything is the best way to make sure you don't miss out. Join our Discord! Discord is a messaging platform that we use to stay in touch with each other, organize our different teams, and update everyone on last minute things. This is a MUST do if you don't want to get left out! Follow these steps to join our Discord server: Click the invite link to enter the Discord server. Introduce yourself on the #introductions channel and friend request one of the moderators so they can chat with you (you can write @moderators in your message to get their attention). They will need to add a role for you as a \"member\" so you can stay on the server before you log out, otherwise you will have to be invited and join again. Install Discord on your computer , Android , or iOS device to always stay up-to-date. Chat with someone! The best way to get involved with the network is to find someone that can direct you! Most of our work happens in teams. Here's how to join a team . Subscribe to our Google calendar! On our Google calendar we post regular occurring meetings and any impromptu events like social events, emergency repairs, site visits, etc. This is one of the only places to find out about the meetings our various teams are having! Join using one of these options: Use this link that should prompt you to add the calendar to your Google account Use this ICS file to manually add the calendar via the iCalendar format. View the calendar here Follow our social media! Our social media team makes posts about upcoming meetings, social events, and they also occasionally make educational posts! Don't miss out and follow us on Instagram , Facebook , and Twitter . Visit our Space!","title":"Join Us"},{"location":"community/join/#join-us","text":"Seattle Community Network has a place for everyone . Whether you'd like to join to get free Internet, get involved to help out your community, learn some skills so that you can get a job, or all of the above! PRO TIP Do everything on this page! Our community is spread out in a lot of different places, so doing everything is the best way to make sure you don't miss out.","title":"Join Us"},{"location":"community/join/#join-our-discord","text":"Discord is a messaging platform that we use to stay in touch with each other, organize our different teams, and update everyone on last minute things. This is a MUST do if you don't want to get left out!","title":"Join our Discord!"},{"location":"community/join/#follow-these-steps-to-join-our-discord-server","text":"Click the invite link to enter the Discord server. Introduce yourself on the #introductions channel and friend request one of the moderators so they can chat with you (you can write @moderators in your message to get their attention). They will need to add a role for you as a \"member\" so you can stay on the server before you log out, otherwise you will have to be invited and join again. Install Discord on your computer , Android , or iOS device to always stay up-to-date.","title":"Follow these steps to join our Discord server:"},{"location":"community/join/#chat-with-someone","text":"The best way to get involved with the network is to find someone that can direct you! Most of our work happens in teams. Here's how to join a team .","title":"Chat with someone!"},{"location":"community/join/#subscribe-to-our-google-calendar","text":"On our Google calendar we post regular occurring meetings and any impromptu events like social events, emergency repairs, site visits, etc. This is one of the only places to find out about the meetings our various teams are having! Join using one of these options: Use this link that should prompt you to add the calendar to your Google account Use this ICS file to manually add the calendar via the iCalendar format. View the calendar here","title":"Subscribe to our Google calendar!"},{"location":"community/join/#follow-our-social-media","text":"Our social media team makes posts about upcoming meetings, social events, and they also occasionally make educational posts! Don't miss out and follow us on Instagram , Facebook , and Twitter .","title":"Follow our social media!"},{"location":"community/join/#visit-our-space","text":"","title":"Visit our Space!"},{"location":"community/mission-vision-values/","text":"Mission, Vision, and Values Mission LCL seeks to democratize knowledge, skills, and resources to enable people to establish and run their own local, community-centered, and community-owned Internet access networks and digital infrastructure. Vision We envision a world where no one is excluded from access to the Internet, and where anyone can achieve the expertise and capability to bring communications infrastructure to their community and improve their quality of life. Values We value the ability to access the Internet and all public information and digital resources therein as a human right. - Digital privacy of our users and partner organizations - Collaboration, especially with the communities and organizations we work with - Care, consideration, allyship, and peer mentorship between individuals within our organization - Education, sharing, and capacity-building- emphasize teaching and dissemination of information and skills - Openness, transparency, and accountability of our organization and its processes - Democratization and inclusiveness of decision processes among stakeholders - Long-term sustainability of our technology deployments and community structures - Equity in planning for resource allocation, programming, and contribution","title":"Mission, Vision, Values"},{"location":"community/mission-vision-values/#mission-vision-and-values","text":"","title":"Mission, Vision, and Values"},{"location":"community/mission-vision-values/#mission","text":"LCL seeks to democratize knowledge, skills, and resources to enable people to establish and run their own local, community-centered, and community-owned Internet access networks and digital infrastructure.","title":"Mission"},{"location":"community/mission-vision-values/#vision","text":"We envision a world where no one is excluded from access to the Internet, and where anyone can achieve the expertise and capability to bring communications infrastructure to their community and improve their quality of life.","title":"Vision"},{"location":"community/mission-vision-values/#values","text":"We value the ability to access the Internet and all public information and digital resources therein as a human right. - Digital privacy of our users and partner organizations - Collaboration, especially with the communities and organizations we work with - Care, consideration, allyship, and peer mentorship between individuals within our organization - Education, sharing, and capacity-building- emphasize teaching and dissemination of information and skills - Openness, transparency, and accountability of our organization and its processes - Democratization and inclusiveness of decision processes among stakeholders - Long-term sustainability of our technology deployments and community structures - Equity in planning for resource allocation, programming, and contribution","title":"Values"},{"location":"community/partners/","text":"Our Partners Althea API Chaya (WiFi is a Lifeline) Black Brilliance Research Project Breakfast Group Cham Refugees Community City of Seattle IT Filipino Community of Seattle King County Library System Oromo Cultural Center Seattle Public Schools Tacoma Cooperative Network Tacoma Public Library University of Washington","title":"Our Partners"},{"location":"community/partners/#our-partners","text":"Althea API Chaya (WiFi is a Lifeline) Black Brilliance Research Project Breakfast Group Cham Refugees Community City of Seattle IT Filipino Community of Seattle King County Library System Oromo Cultural Center Seattle Public Schools Tacoma Cooperative Network Tacoma Public Library University of Washington","title":"Our Partners"},{"location":"community/teams/","text":"Join a Team As a volunteer-run community network, we are always in need of extra hands. If you're interested in helping out, check out what our teams are working on here! Feel free to have informational meetings with the respective team leads to learn more about how you can help. There's no commitment required and we welcome you to take on as much work as you have the capacity for. Outreach Our outreach team is responsible for finding new site host partners, finding users, and maintaining communications with our current partners. Message the #outreach channel in Discord and contact Esther Jang to learn more about how you can get involved. This is the perfect team for you if any of the following apply to you: Experience with community organizing Have community connections in the Greater Seattle Area Have cultural humility and experience partnering with marginalized communities Have the ability to translate and/or interpret into non-English languages common in the Seattle area such as Spanish, Vietnamese, Somali, Oromo, Khmer, and more. As of July 2021, the primary objective of the outreach team is to get connected with users for our network sites that fit any of the following criteria: Unemployed Seniors Housing-unstable or houseless Non-English speaking Social Media Our social media team is in charge of our various accounts on Instagram , Facebook , and Twitter . They also develop branding and marketing materials for our various projects. Message the #social-media channel in Discord and contact Firn Tieanklin to learn more about how you can get involved. You should join this team if you have experience in or are interested in practicing any of the following skills/technologies: - Canva - Design - Marketing - Photography/Videography Web Development Our web development team is working on redesigning and developing our new website. Message the #website channel in Discord to learn more about how you can get involved. You should join this team if you have experience in or are interested in learning any of the following skills/technologies: HTML/CSS Javascript Bootstrap React Redux Web Design (Using Figma) Mobile App Development Our mobile app development team is currently developing an Android app to record cell network performance metrics during our field tests. Message the #measurement channel in Discord and contact Zhennan Zhou to learn more about how you can get involved. You should join this team if you have experience in or are interested in learning any of the following skills/technologies: Android app development iOS app development Java Object oriented programming Open source development, Git, and GitHub Education Our education team plays a core role in our community networks as they enable our networks to be community-owned and operated. The education team is responsible for developing educational materials, running workshops, and teaching our Digital Steward cohorts. Message the #digital-stewards channel in Discord and contact Esther Jang to learn more about how you can get involved. You should join this team if you have experience in or are interested in learning any of the following skills/technologies: Teaching Curriculum development Computer networks, LTE networks, and community networks Linux Community organizing Fundraising Our fundraising team is currently setting up a crowdfunding platform to raise money for various expenses that this project requires. Our fundraising team is also working on applying for various community grants and research grants. Message the #funding channel in Discord and contact Esther Jang to learn more about how you can get involved. Accounting & Legal Local Connectivity Lab, the nonprofit organizing that is incubating this project, often runs into accounting and legal challenges. If you are willing to provide pro bono services to benefit the community network, please contact Esther Jang at lcl@seattlecommunitynetwork.org.","title":"Join a Team"},{"location":"community/teams/#join-a-team","text":"As a volunteer-run community network, we are always in need of extra hands. If you're interested in helping out, check out what our teams are working on here! Feel free to have informational meetings with the respective team leads to learn more about how you can help. There's no commitment required and we welcome you to take on as much work as you have the capacity for.","title":"Join a Team"},{"location":"community/teams/#outreach","text":"Our outreach team is responsible for finding new site host partners, finding users, and maintaining communications with our current partners. Message the #outreach channel in Discord and contact Esther Jang to learn more about how you can get involved. This is the perfect team for you if any of the following apply to you: Experience with community organizing Have community connections in the Greater Seattle Area Have cultural humility and experience partnering with marginalized communities Have the ability to translate and/or interpret into non-English languages common in the Seattle area such as Spanish, Vietnamese, Somali, Oromo, Khmer, and more. As of July 2021, the primary objective of the outreach team is to get connected with users for our network sites that fit any of the following criteria: Unemployed Seniors Housing-unstable or houseless Non-English speaking","title":"Outreach"},{"location":"community/teams/#social-media","text":"Our social media team is in charge of our various accounts on Instagram , Facebook , and Twitter . They also develop branding and marketing materials for our various projects. Message the #social-media channel in Discord and contact Firn Tieanklin to learn more about how you can get involved. You should join this team if you have experience in or are interested in practicing any of the following skills/technologies: - Canva - Design - Marketing - Photography/Videography","title":"Social Media"},{"location":"community/teams/#web-development","text":"Our web development team is working on redesigning and developing our new website. Message the #website channel in Discord to learn more about how you can get involved. You should join this team if you have experience in or are interested in learning any of the following skills/technologies: HTML/CSS Javascript Bootstrap React Redux Web Design (Using Figma)","title":"Web Development"},{"location":"community/teams/#mobile-app-development","text":"Our mobile app development team is currently developing an Android app to record cell network performance metrics during our field tests. Message the #measurement channel in Discord and contact Zhennan Zhou to learn more about how you can get involved. You should join this team if you have experience in or are interested in learning any of the following skills/technologies: Android app development iOS app development Java Object oriented programming Open source development, Git, and GitHub","title":"Mobile App Development"},{"location":"community/teams/#education","text":"Our education team plays a core role in our community networks as they enable our networks to be community-owned and operated. The education team is responsible for developing educational materials, running workshops, and teaching our Digital Steward cohorts. Message the #digital-stewards channel in Discord and contact Esther Jang to learn more about how you can get involved. You should join this team if you have experience in or are interested in learning any of the following skills/technologies: Teaching Curriculum development Computer networks, LTE networks, and community networks Linux Community organizing","title":"Education"},{"location":"community/teams/#fundraising","text":"Our fundraising team is currently setting up a crowdfunding platform to raise money for various expenses that this project requires. Our fundraising team is also working on applying for various community grants and research grants. Message the #funding channel in Discord and contact Esther Jang to learn more about how you can get involved.","title":"Fundraising"},{"location":"community/teams/#accounting-legal","text":"Local Connectivity Lab, the nonprofit organizing that is incubating this project, often runs into accounting and legal challenges. If you are willing to provide pro bono services to benefit the community network, please contact Esther Jang at lcl@seattlecommunitynetwork.org.","title":"Accounting & Legal"},{"location":"community/tech-help/","text":"Community Tech Help There are several ways to get community-based tech support from the SCN community, such as the Help Desk . You can use any of these methods to request Internet service from us, request general technology or computer help, or contact us about any other topic. Join our Discord! The FASTEST way to get support will be to join the #support channel on our Discord , a messaging platform that we use to organize. Follow these steps to join Discord: Join our Discord server via the invite link. You will need to log in or create an account to join, and a moderator will need to assign you a role before you are allowed to join permanently. Install Discord for your computer , Android , or iOS device to stay up-to-date on conversations. When posting to the #support channel, describe your question or problem in as much detail as you can. Someone from the community will most likely respond within a few hours. If you would like to get to know the community, introduce yourself in the #introductions channel! How did you hear about SCN and why are you interested? (More complete instructions can be found here ). Community-Run Help Desk The Seattle Community Network organizes a community-run tech help desk supported by the Black Brilliance Research Project's (BBR's) Digital Stewards Program and the Filipino Community of Seattle (FCS). Our current in-person help desk hours are on Fridays at 3-5 pm starting on 2/18/2022, located in the Filipino Community Village Integrated Learning Center (ILC) . Filipino Community Village Integrated Learning Center address: 5727 37th Ave S, Seattle, WA 98118 Our help desk is best-effort and mainly volunteer-based, so our virtual hours availability may be highly variable. Please check our available hours and sign up for an appointment slot using the calendar link below if you can (or contact us another way if that doesn't work), as it helps the volunteers plan for our time. However, you may also drop in during our scheduled calendar hours without an appointment. Our Help Desk Calendar : You can sign up for an appointment slot for any staffed volunteer hours indicated on the calendar. Phone Number (Voicemail-only except during staffed Community Tech Help hours): (253) 655-7221 You may also send text messages to this number, which will be checked during staffed hours. Inquiries about getting connected to our Internet service can also be sent here. Email address for general Tech Support: support@seattlecommunitynetwork.org Email address for SCN Internet service-related support: help@seattlecommunitynetwork.org Help Desk Volunteers We are actively recruiting more volunteers to help us run the help desk virtually, whenever and wherever that you are available. Join our team by simply signing up on this interest form , we need you! Please let us know if you have additional questions or concerns in the #support channel on our Discord . Documentation As always, please do not hesitate to consult our docs for any information, or submit an issue on the docs site github if there is information missing that you would like to see. Also feel free to message the Discord for the same purpose.","title":"Community Tech Help"},{"location":"community/tech-help/#community-tech-help","text":"There are several ways to get community-based tech support from the SCN community, such as the Help Desk . You can use any of these methods to request Internet service from us, request general technology or computer help, or contact us about any other topic.","title":"Community Tech Help"},{"location":"community/tech-help/#join-our-discord","text":"The FASTEST way to get support will be to join the #support channel on our Discord , a messaging platform that we use to organize.","title":"Join our Discord!"},{"location":"community/tech-help/#follow-these-steps-to-join-discord","text":"Join our Discord server via the invite link. You will need to log in or create an account to join, and a moderator will need to assign you a role before you are allowed to join permanently. Install Discord for your computer , Android , or iOS device to stay up-to-date on conversations. When posting to the #support channel, describe your question or problem in as much detail as you can. Someone from the community will most likely respond within a few hours. If you would like to get to know the community, introduce yourself in the #introductions channel! How did you hear about SCN and why are you interested? (More complete instructions can be found here ).","title":"Follow these steps to join Discord:"},{"location":"community/tech-help/#community-run-help-desk","text":"The Seattle Community Network organizes a community-run tech help desk supported by the Black Brilliance Research Project's (BBR's) Digital Stewards Program and the Filipino Community of Seattle (FCS). Our current in-person help desk hours are on Fridays at 3-5 pm starting on 2/18/2022, located in the Filipino Community Village Integrated Learning Center (ILC) . Filipino Community Village Integrated Learning Center address: 5727 37th Ave S, Seattle, WA 98118 Our help desk is best-effort and mainly volunteer-based, so our virtual hours availability may be highly variable. Please check our available hours and sign up for an appointment slot using the calendar link below if you can (or contact us another way if that doesn't work), as it helps the volunteers plan for our time. However, you may also drop in during our scheduled calendar hours without an appointment. Our Help Desk Calendar : You can sign up for an appointment slot for any staffed volunteer hours indicated on the calendar. Phone Number (Voicemail-only except during staffed Community Tech Help hours): (253) 655-7221 You may also send text messages to this number, which will be checked during staffed hours. Inquiries about getting connected to our Internet service can also be sent here. Email address for general Tech Support: support@seattlecommunitynetwork.org Email address for SCN Internet service-related support: help@seattlecommunitynetwork.org","title":"Community-Run Help Desk"},{"location":"community/tech-help/#help-desk-volunteers","text":"We are actively recruiting more volunteers to help us run the help desk virtually, whenever and wherever that you are available. Join our team by simply signing up on this interest form , we need you! Please let us know if you have additional questions or concerns in the #support channel on our Discord .","title":"Help Desk Volunteers"},{"location":"community/tech-help/#documentation","text":"As always, please do not hesitate to consult our docs for any information, or submit an issue on the docs site github if there is information missing that you would like to see. Also feel free to message the Discord for the same purpose.","title":"Documentation"},{"location":"contribute/contribute/","text":"Contribute to SCN Docs Looking to help? If you wanna share resources and help improve our docs, this page will get you started! Our docs are designed so that anyone can contribute. If this page isn't enough, contact one of us and we'll be able to help you! Our documentation uses MkDocs ReadTheDocs theme (links at the bottom of the page) Editing process To edit this documentation you should: Get your own copy of the repo Modify the documentation in your own repo (for more information see section on Local Development) Submit a pull request Wait for someone to review and accept the request The rest of this page will explain all the details you need to know about the directory structure, markdown, and other quirks for editing this documentation. Make sure to read everything! Markdown files All our documentation is stored in 'Markdown' files so that they can be easily modified and changed without heavy technical knowledge. Markdown Editors A nice and simple online editor is StackEdit which will let you type in markdown and see what it would look like in realtime in split-screen. Another option is HackMD which has the same features as StackEdit but it also allows you to connect to your own GitHub repo and pull/push. This is a nice option if you are uncomfortable with using Git from the command line. Local Development Using MkDocs MkDocs is pretty simple, just install it through pip then you can run mkdocs serve to locally view the website it will generate. Documentation Directory Structure Each directory has a .site file that declares the order each file/folder in that directory will show up. Children pages are implicit in the directory structure Static Files If you need static files for any of your pages, you should put them in the assets folder within the top level docs folder For example, for the cable-crimping page, the images for the tutorial are located in the \"assets/cable-crimping\" folder. These images can then be referenced relatively with the following standard markdown image syntax: ![RJ45 Crimping Tool](../assets/cable-crimping/kit-crimping-tool.jpg)","title":"Contribute to SCN Docs"},{"location":"contribute/contribute/#contribute-to-scn-docs","text":"","title":"Contribute to SCN Docs"},{"location":"contribute/contribute/#looking-to-help","text":"If you wanna share resources and help improve our docs, this page will get you started! Our docs are designed so that anyone can contribute. If this page isn't enough, contact one of us and we'll be able to help you! Our documentation uses MkDocs ReadTheDocs theme (links at the bottom of the page)","title":"Looking to help?"},{"location":"contribute/contribute/#editing-process","text":"To edit this documentation you should: Get your own copy of the repo Modify the documentation in your own repo (for more information see section on Local Development) Submit a pull request Wait for someone to review and accept the request The rest of this page will explain all the details you need to know about the directory structure, markdown, and other quirks for editing this documentation. Make sure to read everything!","title":"Editing process"},{"location":"contribute/contribute/#markdown-files","text":"All our documentation is stored in 'Markdown' files so that they can be easily modified and changed without heavy technical knowledge.","title":"Markdown files"},{"location":"contribute/contribute/#markdown-editors","text":"A nice and simple online editor is StackEdit which will let you type in markdown and see what it would look like in realtime in split-screen. Another option is HackMD which has the same features as StackEdit but it also allows you to connect to your own GitHub repo and pull/push. This is a nice option if you are uncomfortable with using Git from the command line.","title":"Markdown Editors"},{"location":"contribute/contribute/#local-development","text":"","title":"Local Development"},{"location":"contribute/contribute/#using-mkdocs","text":"MkDocs is pretty simple, just install it through pip then you can run mkdocs serve to locally view the website it will generate.","title":"Using MkDocs"},{"location":"contribute/contribute/#documentation-directory-structure","text":"Each directory has a .site file that declares the order each file/folder in that directory will show up. Children pages are implicit in the directory structure","title":"Documentation Directory Structure"},{"location":"contribute/contribute/#static-files","text":"If you need static files for any of your pages, you should put them in the assets folder within the top level docs folder For example, for the cable-crimping page, the images for the tutorial are located in the \"assets/cable-crimping\" folder. These images can then be referenced relatively with the following standard markdown image syntax: ![RJ45 Crimping Tool](../assets/cable-crimping/kit-crimping-tool.jpg)","title":"Static Files"},{"location":"faq/about/","text":"About Seattle Community Network Seattle Community Network (SCN) is a community network dedicated to providing fair access to underserved communities all across the Puget Sound. SCN is a project of Local Connectivity Lab, a 501(c)(3) registered non-profit that works to share free or low-cost broadband access in higher-need areas throughout the Puget Sound region, making use of existing network infrastructure such as buildings and fiber-optic cables to extend coverage to more people. As a community network, we rely on the help of local residents such as yourself to maintain and grow the network. Joining us is a great way to become an active member of your own community, make friends, and learn valuable technical skills.","title":"What is the Seattle Community Network?"},{"location":"faq/about/#about-seattle-community-network","text":"Seattle Community Network (SCN) is a community network dedicated to providing fair access to underserved communities all across the Puget Sound. SCN is a project of Local Connectivity Lab, a 501(c)(3) registered non-profit that works to share free or low-cost broadband access in higher-need areas throughout the Puget Sound region, making use of existing network infrastructure such as buildings and fiber-optic cables to extend coverage to more people. As a community network, we rely on the help of local residents such as yourself to maintain and grow the network. Joining us is a great way to become an active member of your own community, make friends, and learn valuable technical skills.","title":"About Seattle Community Network"},{"location":"faq/connection/","text":"How do I get Internet from the Seattle Community Network? Eligibility The Seattle Community Network exists to provide free or low-cost internet to low-income and in-need users. We prioritize serving the following groups: low-income families of students unemployed adults (looking for work) majority non-English speaking adults/families seniors Registration To connect to the internet through the Seattle Community Network, you will need to register with us. To register, you can: - Email lcl@seattlecommunitynetwork.org - Contact us by phone at (253) 655-7221 and leaving a voice mail or text. Hardware Once your registration is processed, you will receive the necessary hardware to connect to the network.","title":"How do I get Internet?"},{"location":"faq/connection/#how-do-i-get-internet-from-the-seattle-community-network","text":"","title":"How do I get Internet from the Seattle Community Network?"},{"location":"faq/connection/#eligibility","text":"The Seattle Community Network exists to provide free or low-cost internet to low-income and in-need users. We prioritize serving the following groups: low-income families of students unemployed adults (looking for work) majority non-English speaking adults/families seniors","title":"Eligibility"},{"location":"faq/connection/#registration","text":"To connect to the internet through the Seattle Community Network, you will need to register with us. To register, you can: - Email lcl@seattlecommunitynetwork.org - Contact us by phone at (253) 655-7221 and leaving a voice mail or text.","title":"Registration"},{"location":"faq/connection/#hardware","text":"Once your registration is processed, you will receive the necessary hardware to connect to the network.","title":"Hardware"},{"location":"faq/help/","text":"How can I Help? Volunteer! SCN is run completely by volunteers. There are many ways you can help, and no technology skills are required. We need help with everything from setting up network hardware and developing software to community outreach and fundraising. If you want to help, we can use your talents! First, make sure you get connected with our community . Next, why not Join a Team or Contribute to SCN Docs ?","title":"How can I Help?"},{"location":"faq/help/#how-can-i-help","text":"","title":"How can I Help?"},{"location":"faq/help/#volunteer","text":"SCN is run completely by volunteers. There are many ways you can help, and no technology skills are required. We need help with everything from setting up network hardware and developing software to community outreach and fundraising. If you want to help, we can use your talents! First, make sure you get connected with our community . Next, why not Join a Team or Contribute to SCN Docs ?","title":"Volunteer!"},{"location":"faq/how/","text":"How Does the Seattle Community Network Work? The Seattle Community Network partners with the University of Washington to share free or low-cost internet access with areas of higher need. The Seattle Community Network (SCN) is a wireless Internet access network using 4G LTE and WiFi technologies, providing public access from partner locations such as libraries, schools, businesses, and community centers. The Internet connection at these sites is shared wirelessly to nearby devices using the 4G LTE (cell-phone) data standard, which can be used by certain phones and hotspots (also known as Customer Premises Equipment or CPE). Individual users can connect to this signal using SCN-provided (or other compatible) devices to create a local WiFi network in their home. Some of our installed sites utilize a portion of the University of Washington's internet bandwidth, sharing it out to further neighborhoods via point-to-point wireless links. Some sites use other upstream internet service providers (ISPs) such as Lumen. The network is completely created, managed, and maintained by volunteers with a range of diverse skills in information technology and beyond. All infrastructure is paid for by generous donations from sponsors and the public. Speaking of which, why not volunteer or donate ?","title":"How does SCN Work?"},{"location":"faq/how/#how-does-the-seattle-community-network-work","text":"The Seattle Community Network partners with the University of Washington to share free or low-cost internet access with areas of higher need. The Seattle Community Network (SCN) is a wireless Internet access network using 4G LTE and WiFi technologies, providing public access from partner locations such as libraries, schools, businesses, and community centers. The Internet connection at these sites is shared wirelessly to nearby devices using the 4G LTE (cell-phone) data standard, which can be used by certain phones and hotspots (also known as Customer Premises Equipment or CPE). Individual users can connect to this signal using SCN-provided (or other compatible) devices to create a local WiFi network in their home. Some of our installed sites utilize a portion of the University of Washington's internet bandwidth, sharing it out to further neighborhoods via point-to-point wireless links. Some sites use other upstream internet service providers (ISPs) such as Lumen. The network is completely created, managed, and maintained by volunteers with a range of diverse skills in information technology and beyond. All infrastructure is paid for by generous donations from sponsors and the public. Speaking of which, why not volunteer or donate ?","title":"How Does the Seattle Community Network Work?"},{"location":"faq/site/","text":"About This Website The Seattle Community Network Docs website is the central hub for information about our community and networks. Here, we describe our infrastructure, how to set-up hardware and software, how you can start your own community network, our community rules, and more. This website is maintained by our volunteers, much like the rest of our services. This means you can help us improve it by adding missing information, clarifying confusing points, or even just fixing typos you notice while you\u2019re reading. See Contribute to SCN Docs to learn more about how you can contribute to this website. If you are looking for our main website, it is located at www.seattlecommunitynetwork.org .","title":"What is this site?"},{"location":"faq/site/#about-this-website","text":"The Seattle Community Network Docs website is the central hub for information about our community and networks. Here, we describe our infrastructure, how to set-up hardware and software, how you can start your own community network, our community rules, and more. This website is maintained by our volunteers, much like the rest of our services. This means you can help us improve it by adding missing information, clarifying confusing points, or even just fixing typos you notice while you\u2019re reading. See Contribute to SCN Docs to learn more about how you can contribute to this website. If you are looking for our main website, it is located at www.seattlecommunitynetwork.org .","title":"About This Website"},{"location":"faq/what/","text":"What is a Community Network? \"Community Networks (CNs) are crowd-sourced collaborative networks, developed in a bottom-up fashion by groups of individuals \u2013 i.e. communities \u2013 that design, develop and manage the network infrastructure as a common resource. Importantly, at the centre of CNs and the socio-economic ecosystems they generate lay the communities and their members, who are essential to initiate, maintain and guarantee the success of these connectivity efforts.\" Source: Building Community Network Policies: A Collaborative Governance towards Enabling Frameworks","title":"What is a Community Network?"},{"location":"faq/what/#what-is-a-community-network","text":"\"Community Networks (CNs) are crowd-sourced collaborative networks, developed in a bottom-up fashion by groups of individuals \u2013 i.e. communities \u2013 that design, develop and manage the network infrastructure as a common resource. Importantly, at the centre of CNs and the socio-economic ecosystems they generate lay the communities and their members, who are essential to initiate, maintain and guarantee the success of these connectivity efforts.\" Source: Building Community Network Policies: A Collaborative Governance towards Enabling Frameworks","title":"What is a Community Network?"},{"location":"faq/why/","text":"Why Have a Community Network? Internet access is a foundational component of many aspects of modern life, nearly as important as electricity and water service. Though internet service is generally available in many areas of the United States, there are still areas that are underserved because of a variety of geographic and socio-economic factors, or simply because traditional internet service providers do not find it profitable to install and maintain the necessary infrastructure to serve some areas with adequate internet. Community networks attempt to address this digital divide by connecting underserved communities. Because a community network is created, managed, and maintained by volunteers, it is able to serve areas that may not have other affordable internet options.","title":"Why have a Community Network?"},{"location":"faq/why/#why-have-a-community-network","text":"Internet access is a foundational component of many aspects of modern life, nearly as important as electricity and water service. Though internet service is generally available in many areas of the United States, there are still areas that are underserved because of a variety of geographic and socio-economic factors, or simply because traditional internet service providers do not find it profitable to install and maintain the necessary infrastructure to serve some areas with adequate internet. Community networks attempt to address this digital divide by connecting underserved communities. Because a community network is created, managed, and maintained by volunteers, it is able to serve areas that may not have other affordable internet options.","title":"Why Have a Community Network?"},{"location":"infrastructure/epc-setup/","text":"Step 1: CoLTE/EPC (LTE Core Network) Setup Our core networks use the CoLTE project maintained by the UW ICTD Lab . For information on how to install and configure CoLTE, visit the tutorial we wrote with them!","title":"Step 1. LTE Core Network Setup"},{"location":"infrastructure/epc-setup/#step-1-colteepc-lte-core-network-setup","text":"Our core networks use the CoLTE project maintained by the UW ICTD Lab . For information on how to install and configure CoLTE, visit the tutorial we wrote with them!","title":"Step 1: CoLTE/EPC (LTE Core Network) Setup"},{"location":"infrastructure/hardware/","text":"Our Hardware This page will be an overview of some of the core pieces of hardware that we use to deploy our sites. This page is in development, please contact us at lcl@seattlecommunitynetwork.org if you would like to learn more about the hardware we use. TODO Network Site Equipment Base Station (eNodeB) Baicells Nova 233 3.5GHz 1W Gen2 More info here Panel Antennas (eNodeB) Alpha Wireless, 3.3-3.8GHz, 2x2 MIMO, 18dBi, +/-45\u00b0, 65\u00b0 More info here Core Network Computer (EPC) Qotom Mini PC Q190G4N S07 Key features: - 4 ethernet ports - designed to be run 24/7 - small and quiet - cheap More info here User Access Devices LTE Consumer Premises Equipment (CPE) Baicells Atom OD04 3.5GHz 14dBi More info here Outdoor WiFi Router Mikrotik OmniTIK 5 PoE ac Outdoor router of choice for NYC Mesh, so it has been tried and tested. Good balance of quality and price. More info here Home WiFi Router TP-Link Archer A5 Router More info here CBRS-Compatible Unlocked Smartphone We purchase refurbished Google Pixel 4 smartphones because they are affordable, provide all necessary smartphone features, and are CBRS-compatible. Note that purchasing CBRS-compatible phones can be a logistical challenge. We've experienced trouble purchasing from vendors that send incorrect models of phones that don't support CBRS band and we had to go back and forth. Test your phones before distributing them! Here is one spot to purchase refurbished phones.","title":"Hardware Overview"},{"location":"infrastructure/hardware/#our-hardware","text":"This page will be an overview of some of the core pieces of hardware that we use to deploy our sites. This page is in development, please contact us at lcl@seattlecommunitynetwork.org if you would like to learn more about the hardware we use.","title":"Our Hardware"},{"location":"infrastructure/hardware/#todo","text":"","title":"TODO"},{"location":"infrastructure/hardware/#network-site-equipment","text":"","title":"Network Site Equipment"},{"location":"infrastructure/hardware/#base-station-enodeb","text":"Baicells Nova 233 3.5GHz 1W Gen2 More info here","title":"Base Station (eNodeB)"},{"location":"infrastructure/hardware/#panel-antennas-enodeb","text":"Alpha Wireless, 3.3-3.8GHz, 2x2 MIMO, 18dBi, +/-45\u00b0, 65\u00b0 More info here","title":"Panel Antennas (eNodeB)"},{"location":"infrastructure/hardware/#core-network-computer-epc","text":"Qotom Mini PC Q190G4N S07 Key features: - 4 ethernet ports - designed to be run 24/7 - small and quiet - cheap More info here","title":"Core Network Computer (EPC)"},{"location":"infrastructure/hardware/#user-access-devices","text":"","title":"User Access Devices"},{"location":"infrastructure/hardware/#lte-consumer-premises-equipment-cpe","text":"Baicells Atom OD04 3.5GHz 14dBi More info here","title":"LTE Consumer Premises Equipment (CPE)"},{"location":"infrastructure/hardware/#outdoor-wifi-router","text":"Mikrotik OmniTIK 5 PoE ac Outdoor router of choice for NYC Mesh, so it has been tried and tested. Good balance of quality and price. More info here","title":"Outdoor WiFi Router"},{"location":"infrastructure/hardware/#home-wifi-router","text":"TP-Link Archer A5 Router More info here","title":"Home WiFi Router"},{"location":"infrastructure/hardware/#cbrs-compatible-unlocked-smartphone","text":"We purchase refurbished Google Pixel 4 smartphones because they are affordable, provide all necessary smartphone features, and are CBRS-compatible. Note that purchasing CBRS-compatible phones can be a logistical challenge. We've experienced trouble purchasing from vendors that send incorrect models of phones that don't support CBRS band and we had to go back and forth. Test your phones before distributing them! Here is one spot to purchase refurbished phones.","title":"CBRS-Compatible Unlocked Smartphone"},{"location":"infrastructure/librenms-manager-setup/","text":"LibreNMS Network Manager Configuration Seattle Community Networks uses SNMP to monitor network nodes. LibreNMS is used for Network Management, Dashboard generation and Alerting. LibreNMS Manager Installation: Install LibreNMS Install and Configure LibreNMS on Ubuntu with nginx Network-Specific Configuration: Change active user to librenms: sudo su - librenms Edit /opt/librenms/config.php: '); $config['auth_mechanism'] = \"mysql\"; # default, other options: ldap, http-auth $config['nets'][] = \"10.0.0.0/24\"; # Replace with your Management Network Subdomain $config['rrd_purge'] = 0; $config['enable_billing'] = 1; $config['show_services'] = 1; As user 'librenms', run /opt/librenms/snmp-scan.php, to scan the configured network for snmp hosts Adding Baicells OS configuration to LibreNMS As user 'librenms' on the librenms server, create the following files and update their contents accordingly: * For OS detection, ~librenms/includes/definitions/rts.yaml: os: rts text: 'Baicells RTS' type: network icon: rts over: - { graph: device_bits, text: 'Device Traffic' } - { graph: device_processor, text: 'CPU Usage' } - { graph: device_mempool, text: 'Memory Usage' } discovery: - sysDescr: - 'CELL' For defining custom RTS OS sensors, ~librenms/includes/definitions/discovery/rts.yaml: mib: BAICELLS-MIB modules: os: hardware: BAICELLS-MIB::hardwareVersion.0 serial: BAICELLS-MIB::sn.0 version: BAICELLS-MIB::softwareVersion.0 sensors: count: data: - oid: ulThroughput num_oid: '.1.3.6.1.4.1.53058.190.7.{{ $index }}' descr: 'Upload Throughput' group: 'Throughput' index: 'ulthroughput.{{ $index }}' - oid: dlThroughput num_oid: '.1.3.6.1.4.1.53058.190.8.{{ $index }}' descr: 'Download Throughput' group: 'Throughput' index: 'dlThroughput.{{ $index }}' - oid: ulPrbUtilization num_oid: '.1.3.6.1.4.1.53058.190.9.{{ $index }}' descr: 'Upload PRB Utilization' group: 'Utilization' index: 'ulPrbUtilization{{ $index }}' - oid: dlPrbUtilization num_oid: '.1.3.6.1.4.1.53058.190.10.{{ $index }}' descr: 'Download PRB Utilization' group: 'Utilization' index: 'dlPrbUtilization.{{ $index }}' frequency: data: - oid: carrierBwMhz num_oid: '.1.3.6.1.4.1.53058.100.7.{{ $index }}' divisor: 5 descr: 'Carrier Bandwidth' index: 'carrierBwMhz.{{ $index }}' percent: data: - oid: eRABEstablishSuccessRate num_oid: '.1.3.6.1.4.1.53058.190.3.{{ $index }}' descr: 'ERAB Establishment Success Rate' group: 'LTE' index: 'eRABEstablishSuccessRate.{{ $index }}' - oid: hoSuccInterEnbS1Rate num_oid: '.1.3.6.1.4.1.53058.190.4.{{ $index }}' descr: 'Inter MME S1 Handover Success Rate' group: 'LTE' index: 'heSuccInterEnbS1Rate.{{ $index }}' - oid: hoSuccInterEnbRate num_oid: '.1.3.6.1.4.1.53058.190.5.{{ $index }}' descr: 'Inter MME Handover Success Rate' group: 'LTE' index: 'hoSuccInterEnbRate.{{ $index }}' - oid: rrcBuildSuccessRate num_oid: '.1.3.6.1.4.1.53058.190.6.{{ $index }}' descr: 'RRC Build Success Rate' group: 'LTE' index: 'rrcBuildSuccessRate.{{ $index }}' For defining a custom OS class to use Wireless sensors, ~librenms/LibreNMS/OS/Rts.php (note: pay attention to capitalization) getDeviceId(), $oid, 'rts', 1, 'UE Connections') ); } } A nice looking logo, ~librenms/html/images/os/rts.png Download an example Baicells Logo Here Download the baicells mib from this link , and save it to ~librenms/mibs/BAICELLS-MIB (note: no file extension)","title":"Network Monitoring 1. LibreNMS Network Manager Configuration"},{"location":"infrastructure/librenms-manager-setup/#librenms-network-manager-configuration","text":"Seattle Community Networks uses SNMP to monitor network nodes. LibreNMS is used for Network Management, Dashboard generation and Alerting.","title":"LibreNMS Network Manager Configuration"},{"location":"infrastructure/librenms-manager-setup/#librenms-manager-installation","text":"Install LibreNMS Install and Configure LibreNMS on Ubuntu with nginx","title":"LibreNMS Manager Installation:"},{"location":"infrastructure/librenms-manager-setup/#network-specific-configuration","text":"Change active user to librenms: sudo su - librenms Edit /opt/librenms/config.php: '); $config['auth_mechanism'] = \"mysql\"; # default, other options: ldap, http-auth $config['nets'][] = \"10.0.0.0/24\"; # Replace with your Management Network Subdomain $config['rrd_purge'] = 0; $config['enable_billing'] = 1; $config['show_services'] = 1; As user 'librenms', run /opt/librenms/snmp-scan.php, to scan the configured network for snmp hosts","title":"Network-Specific Configuration:"},{"location":"infrastructure/librenms-manager-setup/#adding-baicells-os-configuration-to-librenms","text":"As user 'librenms' on the librenms server, create the following files and update their contents accordingly: * For OS detection, ~librenms/includes/definitions/rts.yaml: os: rts text: 'Baicells RTS' type: network icon: rts over: - { graph: device_bits, text: 'Device Traffic' } - { graph: device_processor, text: 'CPU Usage' } - { graph: device_mempool, text: 'Memory Usage' } discovery: - sysDescr: - 'CELL' For defining custom RTS OS sensors, ~librenms/includes/definitions/discovery/rts.yaml: mib: BAICELLS-MIB modules: os: hardware: BAICELLS-MIB::hardwareVersion.0 serial: BAICELLS-MIB::sn.0 version: BAICELLS-MIB::softwareVersion.0 sensors: count: data: - oid: ulThroughput num_oid: '.1.3.6.1.4.1.53058.190.7.{{ $index }}' descr: 'Upload Throughput' group: 'Throughput' index: 'ulthroughput.{{ $index }}' - oid: dlThroughput num_oid: '.1.3.6.1.4.1.53058.190.8.{{ $index }}' descr: 'Download Throughput' group: 'Throughput' index: 'dlThroughput.{{ $index }}' - oid: ulPrbUtilization num_oid: '.1.3.6.1.4.1.53058.190.9.{{ $index }}' descr: 'Upload PRB Utilization' group: 'Utilization' index: 'ulPrbUtilization{{ $index }}' - oid: dlPrbUtilization num_oid: '.1.3.6.1.4.1.53058.190.10.{{ $index }}' descr: 'Download PRB Utilization' group: 'Utilization' index: 'dlPrbUtilization.{{ $index }}' frequency: data: - oid: carrierBwMhz num_oid: '.1.3.6.1.4.1.53058.100.7.{{ $index }}' divisor: 5 descr: 'Carrier Bandwidth' index: 'carrierBwMhz.{{ $index }}' percent: data: - oid: eRABEstablishSuccessRate num_oid: '.1.3.6.1.4.1.53058.190.3.{{ $index }}' descr: 'ERAB Establishment Success Rate' group: 'LTE' index: 'eRABEstablishSuccessRate.{{ $index }}' - oid: hoSuccInterEnbS1Rate num_oid: '.1.3.6.1.4.1.53058.190.4.{{ $index }}' descr: 'Inter MME S1 Handover Success Rate' group: 'LTE' index: 'heSuccInterEnbS1Rate.{{ $index }}' - oid: hoSuccInterEnbRate num_oid: '.1.3.6.1.4.1.53058.190.5.{{ $index }}' descr: 'Inter MME Handover Success Rate' group: 'LTE' index: 'hoSuccInterEnbRate.{{ $index }}' - oid: rrcBuildSuccessRate num_oid: '.1.3.6.1.4.1.53058.190.6.{{ $index }}' descr: 'RRC Build Success Rate' group: 'LTE' index: 'rrcBuildSuccessRate.{{ $index }}' For defining a custom OS class to use Wireless sensors, ~librenms/LibreNMS/OS/Rts.php (note: pay attention to capitalization) getDeviceId(), $oid, 'rts', 1, 'UE Connections') ); } } A nice looking logo, ~librenms/html/images/os/rts.png Download an example Baicells Logo Here Download the baicells mib from this link , and save it to ~librenms/mibs/BAICELLS-MIB (note: no file extension)","title":"Adding Baicells OS configuration to LibreNMS"},{"location":"infrastructure/librenms-setup/","text":"LibreNMS Agent Configuration Adding a New Node to LibreNMS Both the eNodeB and the ePC must be configured individually in order for them to report statistics to the SNMP Manager. Since the eNodeB is not directly accessible from the management VPN, we configure an SNMP proxy on the ePC to pass SNMP statistics to the Management host. ePC SNMP Configuration Install snmpd to the ePC node: $ sudo apt install snmpd Modify /etc/snmp/snmpd.conf: sysLocation sysContact lcl@seattlecommunitynetwork.org sysServices 72 master agentx agentAddress udp:161 com2sec readonly com2sec -Cn ctx_baicells readonly enodeb group readonlygroup v2c readonly view all included .1 access readonlygroup \"\" v2c noauth exact all none none access readonlygroup ctx_baicells v2c noauth prefix all none none proxy -Cn ctx_baicells -v 2c -c private 192.168.151.1 .1.3 This configuration allows us to access SNMP data on the EPC with the standard community string (refer to internal standards documentation). but will proxy the Baicells SNMP data when we send the community string \u2018enodeb\u2019 Update the snmpd service file to automatically restart snmpd on crash: Edit /lib/systemd/system/snmpd.service, modify the 'ExecStart' line, and add the 'ExecReload', 'Restart', and 'RestartSec' lines: [Unit] Description=Simple Network Management Protocol (SNMP) Daemon. After=network.target ConditionPathExists=/etc/snmp/snmpd.conf [Service] Type=simple ExecStartPre=/bin/mkdir -p /var/run/agentx ExecStart=/usr/sbin/snmpd -LO2w -u Debian-snmp -g Debian-snmp -I -smux,mteTrigger,mteTriggerConf -f -p /run/snmpd.pid ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target Enable and restart snmpd: Sudo systemctl daemon-reload sudo systemctl enable snmpd sudo systemctl restart snmpd Baicells SNMP configuration Log into the Baicells configuration console: https:// From the left menu, select System Select SNMP Under \u2018SNMP Switch,\u2019 select \u2018Enable\u2019 Configure the following options: Community String: private Contact: lcl@seattlecommunitynetwork.org Location: \\ (String should not have any spaces) Source: Any Adding the Node to LibreNMS If the ePC is running, librenms should be able to auto-discover it. Run this command from a shell on the management host: sudo -u librenms lnms scan LibreNMS should print a status message that it was able to add a new device. When first discovered, the ePC will show up generically as it\u2019s ip address. Edit the hostname, but clicking \u2018Edit Device\u2019 (gear icon): Click the red pencil icon, and change the ip address to the hostname Fill \u2018Overwrite IP\u2019 with the ePC IP address Note: If the IP is not changed to the hostname, you will not be able to add the eNodeB by it\u2019s IP address The Baicells eNB needs to be added manually: From LibreNMS, select Devices and click \u201cAdd Device\u201d Add a new device, with the following configurations: Hostname: Community: \u2018enodeb\u2019 Force Add: On Note: If you receive an error message stating that a device with the specified IP already exists, make sure that you have successfully changed the eNodeB\u2019s hostname per the previous step. Once the device is added, click the \u2018Edit Device\u2019 icon (gear icon) and update the following values: Display name: \\ Overwrite device contact: lcl@seattlecommunitynetwork.org Other helpful notes: Baicells eNB config guide How to SSH into Baicells eNB: SSH using port 27149 (username same as normal web-based login) Convert the MAC address of this eNB to link local address: http://www.sput.nl/internet/ipv6/ll-mac.html","title":"Network Monitoring 2. LibreNMS Agent Configuration"},{"location":"infrastructure/librenms-setup/#librenms-agent-configuration","text":"","title":"LibreNMS Agent Configuration"},{"location":"infrastructure/librenms-setup/#adding-a-new-node-to-librenms","text":"Both the eNodeB and the ePC must be configured individually in order for them to report statistics to the SNMP Manager. Since the eNodeB is not directly accessible from the management VPN, we configure an SNMP proxy on the ePC to pass SNMP statistics to the Management host.","title":"Adding a New Node to LibreNMS"},{"location":"infrastructure/librenms-setup/#epc-snmp-configuration","text":"Install snmpd to the ePC node: $ sudo apt install snmpd Modify /etc/snmp/snmpd.conf: sysLocation sysContact lcl@seattlecommunitynetwork.org sysServices 72 master agentx agentAddress udp:161 com2sec readonly com2sec -Cn ctx_baicells readonly enodeb group readonlygroup v2c readonly view all included .1 access readonlygroup \"\" v2c noauth exact all none none access readonlygroup ctx_baicells v2c noauth prefix all none none proxy -Cn ctx_baicells -v 2c -c private 192.168.151.1 .1.3 This configuration allows us to access SNMP data on the EPC with the standard community string (refer to internal standards documentation). but will proxy the Baicells SNMP data when we send the community string \u2018enodeb\u2019 Update the snmpd service file to automatically restart snmpd on crash: Edit /lib/systemd/system/snmpd.service, modify the 'ExecStart' line, and add the 'ExecReload', 'Restart', and 'RestartSec' lines: [Unit] Description=Simple Network Management Protocol (SNMP) Daemon. After=network.target ConditionPathExists=/etc/snmp/snmpd.conf [Service] Type=simple ExecStartPre=/bin/mkdir -p /var/run/agentx ExecStart=/usr/sbin/snmpd -LO2w -u Debian-snmp -g Debian-snmp -I -smux,mteTrigger,mteTriggerConf -f -p /run/snmpd.pid ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target Enable and restart snmpd: Sudo systemctl daemon-reload sudo systemctl enable snmpd sudo systemctl restart snmpd","title":"ePC SNMP Configuration"},{"location":"infrastructure/librenms-setup/#baicells-snmp-configuration","text":"Log into the Baicells configuration console: https:// From the left menu, select System Select SNMP Under \u2018SNMP Switch,\u2019 select \u2018Enable\u2019 Configure the following options: Community String: private Contact: lcl@seattlecommunitynetwork.org Location: \\ (String should not have any spaces) Source: Any","title":"Baicells SNMP configuration"},{"location":"infrastructure/librenms-setup/#adding-the-node-to-librenms","text":"If the ePC is running, librenms should be able to auto-discover it. Run this command from a shell on the management host: sudo -u librenms lnms scan LibreNMS should print a status message that it was able to add a new device. When first discovered, the ePC will show up generically as it\u2019s ip address. Edit the hostname, but clicking \u2018Edit Device\u2019 (gear icon): Click the red pencil icon, and change the ip address to the hostname Fill \u2018Overwrite IP\u2019 with the ePC IP address Note: If the IP is not changed to the hostname, you will not be able to add the eNodeB by it\u2019s IP address The Baicells eNB needs to be added manually: From LibreNMS, select Devices and click \u201cAdd Device\u201d Add a new device, with the following configurations: Hostname: Community: \u2018enodeb\u2019 Force Add: On Note: If you receive an error message stating that a device with the specified IP already exists, make sure that you have successfully changed the eNodeB\u2019s hostname per the previous step. Once the device is added, click the \u2018Edit Device\u2019 icon (gear icon) and update the following values: Display name: \\ Overwrite device contact: lcl@seattlecommunitynetwork.org","title":"Adding the Node to LibreNMS"},{"location":"infrastructure/librenms-setup/#other-helpful-notes","text":"Baicells eNB config guide How to SSH into Baicells eNB: SSH using port 27149 (username same as normal web-based login) Convert the MAC address of this eNB to link local address: http://www.sput.nl/internet/ipv6/ll-mac.html","title":"Other helpful notes:"},{"location":"infrastructure/peering/","text":"Public ASN Peering Local Connectivity Lab operates AS54429 Our peering Policy is Yes Please contact us to peer with our network. Note this network is our public ASN, not the Seattle Community Network itself. If you would like to join the network visit our connect page. Seattle Community Network (SCN) is a community network dedicated to providing fair access to underserved communities all across the Puget Sound. Learn more on our FAQ . Peering Policy Local Connectivity Lab has an open peering policy. We have no requirements in terms of traffic, size, support/SLA, etc. We operate both IPv4 and IPv6. Peering via both protocols is appreciated. Locations Building Address Ports Westin 2001 6th Avenue, Seattle, WA 1G / 10G Exchanges Exchange City IPv4 IPv6 ASNs Routes Speed Seattle Internet Exchange (SIX) Seattle, WA 206.81.81.150 2001:504:16::d49d 336 ~192K 10G Peering Data ASN: 54429 Peering Contact: tech@seattlecommunitynetwork.org PeerDB Page: https://as54429.peeringdb.com As we are a non-profit, please consider providing as many routes as possible, including upstream or other routes.","title":"Public ASN Peering"},{"location":"infrastructure/peering/#public-asn-peering","text":"","title":"Public ASN Peering"},{"location":"infrastructure/peering/#local-connectivity-lab-operates-as54429","text":"","title":"Local Connectivity Lab operates AS54429"},{"location":"infrastructure/peering/#our-peering-policy-is-yes","text":"Please contact us to peer with our network. Note this network is our public ASN, not the Seattle Community Network itself. If you would like to join the network visit our connect page. Seattle Community Network (SCN) is a community network dedicated to providing fair access to underserved communities all across the Puget Sound. Learn more on our FAQ .","title":"Our peering Policy is Yes"},{"location":"infrastructure/peering/#peering-policy","text":"Local Connectivity Lab has an open peering policy. We have no requirements in terms of traffic, size, support/SLA, etc. We operate both IPv4 and IPv6. Peering via both protocols is appreciated.","title":"Peering Policy"},{"location":"infrastructure/peering/#locations","text":"Building Address Ports Westin 2001 6th Avenue, Seattle, WA 1G / 10G","title":"Locations"},{"location":"infrastructure/peering/#exchanges","text":"Exchange City IPv4 IPv6 ASNs Routes Speed Seattle Internet Exchange (SIX) Seattle, WA 206.81.81.150 2001:504:16::d49d 336 ~192K 10G","title":"Exchanges"},{"location":"infrastructure/peering/#peering-data","text":"ASN: 54429 Peering Contact: tech@seattlecommunitynetwork.org PeerDB Page: https://as54429.peeringdb.com As we are a non-profit, please consider providing as many routes as possible, including upstream or other routes.","title":"Peering Data"},{"location":"infrastructure/proxmox-vaultwarden-deployment/","text":"Deployed by: Esther Jang, Paul Phillion, Rudra Singh Section 1: What is Proxmox? Proxmox VE (Virtual Environment) is an open-source server management platform designed to deploy and manage virtual machines (VMs) and containers. It integrates KVM hypervisor and LXC containers, enabling users to manage virtual infrastructure through a web-based interface. Section 2: Access Requirements for Proxmox VE at Seattle Community Network To submit a request for a SCN self-hosted Proxmox VM on our private cloud, please fill out this form . If you are working on a project for SCN and need access to the Proxmox VE, please continue reading. Next, you will need access to the OpenVPN. Proxmox VE can only be accessed on the VPN. Specific details can be obtained from the SCN discord. Once connected to the VPN, a specific IP will be provided for you to access the Proxmox VE, where you can input your credentials. Section 3: Setting up your VM Install SSH Install Keys Test SSH CLI Section 4: SSH Troubleshooting Please let the SCN discord know if you have trouble SSH'ing in. A useful command during setup was sudo ufw status . If it is active, use ufw disable . The expected status should be inactive. If that still doesn\u2019t work, try restarting the VM from the Proxmox VE. Section 5: Beginning Deployment: Vaultwarden docker pull vaultwarden/server:latest docker run -d --name vaultwarden -v /vw-data/:/data/ --restart unless-stopped -p 80:80 vaultwarden/server:latest Section 6: Docker Compose Create a Docker Compose file: version: \"3\" services: vaultwarden: container_name: vaultwarden hostname: vaultwarden9 ports: - \"127.0.0.1:8080:80\" environment: - LOG_FILE=/log/access.log - LOG_LEVEL=info - EXTENDED_LOGGING=true image: vaultwarden/server:latest restart: unless-stopped volumes: - /opt/vw-data:/data - /var/log/vw:/log To start and run your container application in detached mode, use: docker-compose up -d docker-compose start docker-compose stop docker-compose restart Section 7: Azure DNS For this deployment, we're utilizing a public IP address. This address must be configured within Azure DNS to point to our chosen domain name. Navigate to Home - Microsoft Azure with your account. Open \u201cDNS zones\u201d. If you were using a private IP, you would use \u201cPrivate DNS zones\u201d. We will be creating a subdomain under seattlecommunitynetwork.org. Click on that, and under DNS management, click recordsets, and click add. Insert the beginning of the subdomain name, which in our case is \u201cvaultwarden\u201d. Below, under IP address, insert your IP, and create your new subdomain. Section 8: Enabling Nginx sudo apt update sudo apt install nginx sudo systemctl enable nginx sudo systemctl start nginx Next, go to /etc/nginx/sites-enabled/default . Delete everything inside default and paste this: # The `upstream` directives ensure that you have a http/1.1 connection # This enables the keepalive option and better performance # # Define the server IP and ports here. upstream vaultwarden-default { zone vaultwarden-default 64k; server 127.0.0.1:8080; keepalive 2; } # Needed to support websocket connections # See: https://nginx.org/en/docs/http/websocket.html # Instead of \"close\" as stated in the above link we send an empty value. # Else all keepalive connections will not work. map $http_upgrade $connection_upgrade { default upgrade; '' \"\"; } # Redirect HTTP to HTTPS server { listen 80; listen [::]:80; server_name vaultwarden.seattlecommunitynetwork.org; if ($host = vaultwarden.seattlecommunitynetwork.org) { return 301 https://$host$request_uri; } return 404; } server { # For older versions of nginx appened http2 to the listen line after ssl and remove `http2 on` listen 443 ssl; listen [::]:443 ssl; # http2 on; server_name vaultwarden.seattlecommunitynetwork.org; # Specify SSL Config when needed ssl_certificate /etc/path...; ssl_certificate_key /etc/path...; ssl_trusted_certificate /etc/path...; client_max_body_size 525M; location / { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://vaultwarden-default; } # Optionally add extra authentication besides the ADMIN_TOKEN # Remove the comments below `#` and create the htpasswd_file to have it active # #location /admin { # See: https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/ #auth_basic \"Administrator's Area\"; #3auth_basic_user_file /path/to/htpasswd_file; #proxy_http_version 1.1; #proxy_set_header Upgrade $http_upgrade; #proxy_set_header Connection $connection_upgrade; #proxy_set_header Host $host; #proxy_set_header X-Real-IP $remote_addr; #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #proxy_set_header X-Forwarded-Proto $scheme; #proxy_pass http://vaultwarden-default; } } Section 9: Obtain SSL Certificates sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d vaultwarden.seattlecommunitynetwork.org Certbot will modify your Nginx configuration to handle HTTPS and redirect from HTTP to HTTPS. Ensure that you copy the file paths provided at the conclusion of the certbot process into the default nginx configuration file, replacing the corresponding comments with these paths. Section 10: Ensure Everything is Running sudo systemctl status nginx Section 11: Additional Features Enabling admin panel: Go back to /etc/nginx/sites-enabled/default and uncomment the admin section at the bottom. Follow directions at Nginx Admin Guide to encrypt your admin password as a .env file (preferably using argon CLI). Once done, make sure you create a .env file in the directory where the compose file is with VAULTWARDEN_ADMIN_TOKEN=[insert your hashed admin token] . Then in your compose, add these two lines under environment: - ADMIN_TOKEN=${VAULTWARDEN_ADMIN_TOKEN} - DOMAIN=https://vaultwarden.seattlecommunitynetwork.org Restart the container and try logging into https://vaultwarden.seattlecommunitynetwork.org/admin . Once logged in, SMTP and 2FA enabling settings can be configured on the home page. Section 12: Data Backup Enabling backups for Vaultwarden is a simple process. Please review the attached backup bash script, which facilitates the transfer of Vaultwarden data to another virtual machine. Additionally, there is a cleanup bash script designed to retain only the most recent file in the other VM, deleting all others. Feel free to modify the scripts as necessary to suit your specific requirements. Backup script: #!/bin/bash docker-compose down datestamp=$(date +%m-%d-%Y) zip -9 -r /home/scn/backups/${datestamp}.zip /opt/vw-data* scp -i ~/.ssh-comm/id_rsa /home/scn/backups/${datestamp}.zip azureuser@[IP address]:~/backups/ docker-compose up -d Cleanup Script: #!/bin/bash # Define the directory containing backup files backup_dir=~/backups # Go to the backup directory cd \"$backup_dir\" || exit # Find and delete older backup files (excluding the latest day) find . -type f -name '*.zip' ! -mtime -1 -exec rm {} + # Exit exit 0 Section 13: Using the Backup To restore a data backup to the original virtual machine, simply unzip the file and delete the existing contents of /opt/vw-data . Then, transfer the contents of your zip file into this directory. Perform a quick restart of the container, and you will have successfully restored the version of the backup you selected.","title":"Proxmox Deployment Guide - Vaultwarden"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#deployed-by-esther-jang-paul-phillion-rudra-singh","text":"","title":"Deployed by: Esther Jang, Paul Phillion, Rudra Singh"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-1-what-is-proxmox","text":"Proxmox VE (Virtual Environment) is an open-source server management platform designed to deploy and manage virtual machines (VMs) and containers. It integrates KVM hypervisor and LXC containers, enabling users to manage virtual infrastructure through a web-based interface.","title":"Section 1: What is Proxmox?"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-2-access-requirements-for-proxmox-ve-at-seattle-community-network","text":"To submit a request for a SCN self-hosted Proxmox VM on our private cloud, please fill out this form . If you are working on a project for SCN and need access to the Proxmox VE, please continue reading. Next, you will need access to the OpenVPN. Proxmox VE can only be accessed on the VPN. Specific details can be obtained from the SCN discord. Once connected to the VPN, a specific IP will be provided for you to access the Proxmox VE, where you can input your credentials.","title":"Section 2: Access Requirements for Proxmox VE at Seattle Community Network"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-3-setting-up-your-vm","text":"","title":"Section 3: Setting up your VM"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#install-ssh","text":"","title":"Install SSH"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#install-keys","text":"","title":"Install Keys"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#test-ssh-cli","text":"","title":"Test SSH CLI"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-4-ssh-troubleshooting","text":"Please let the SCN discord know if you have trouble SSH'ing in. A useful command during setup was sudo ufw status . If it is active, use ufw disable . The expected status should be inactive. If that still doesn\u2019t work, try restarting the VM from the Proxmox VE.","title":"Section 4: SSH Troubleshooting"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-5-beginning-deployment-vaultwarden","text":"docker pull vaultwarden/server:latest docker run -d --name vaultwarden -v /vw-data/:/data/ --restart unless-stopped -p 80:80 vaultwarden/server:latest","title":"Section 5: Beginning Deployment: Vaultwarden"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-6-docker-compose","text":"Create a Docker Compose file: version: \"3\" services: vaultwarden: container_name: vaultwarden hostname: vaultwarden9 ports: - \"127.0.0.1:8080:80\" environment: - LOG_FILE=/log/access.log - LOG_LEVEL=info - EXTENDED_LOGGING=true image: vaultwarden/server:latest restart: unless-stopped volumes: - /opt/vw-data:/data - /var/log/vw:/log To start and run your container application in detached mode, use: docker-compose up -d docker-compose start docker-compose stop docker-compose restart","title":"Section 6: Docker Compose"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-7-azure-dns","text":"For this deployment, we're utilizing a public IP address. This address must be configured within Azure DNS to point to our chosen domain name. Navigate to Home - Microsoft Azure with your account. Open \u201cDNS zones\u201d. If you were using a private IP, you would use \u201cPrivate DNS zones\u201d. We will be creating a subdomain under seattlecommunitynetwork.org. Click on that, and under DNS management, click recordsets, and click add. Insert the beginning of the subdomain name, which in our case is \u201cvaultwarden\u201d. Below, under IP address, insert your IP, and create your new subdomain.","title":"Section 7: Azure DNS"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-8-enabling-nginx","text":"sudo apt update sudo apt install nginx sudo systemctl enable nginx sudo systemctl start nginx Next, go to /etc/nginx/sites-enabled/default . Delete everything inside default and paste this: # The `upstream` directives ensure that you have a http/1.1 connection # This enables the keepalive option and better performance # # Define the server IP and ports here. upstream vaultwarden-default { zone vaultwarden-default 64k; server 127.0.0.1:8080; keepalive 2; } # Needed to support websocket connections # See: https://nginx.org/en/docs/http/websocket.html # Instead of \"close\" as stated in the above link we send an empty value. # Else all keepalive connections will not work. map $http_upgrade $connection_upgrade { default upgrade; '' \"\"; } # Redirect HTTP to HTTPS server { listen 80; listen [::]:80; server_name vaultwarden.seattlecommunitynetwork.org; if ($host = vaultwarden.seattlecommunitynetwork.org) { return 301 https://$host$request_uri; } return 404; } server { # For older versions of nginx appened http2 to the listen line after ssl and remove `http2 on` listen 443 ssl; listen [::]:443 ssl; # http2 on; server_name vaultwarden.seattlecommunitynetwork.org; # Specify SSL Config when needed ssl_certificate /etc/path...; ssl_certificate_key /etc/path...; ssl_trusted_certificate /etc/path...; client_max_body_size 525M; location / { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://vaultwarden-default; } # Optionally add extra authentication besides the ADMIN_TOKEN # Remove the comments below `#` and create the htpasswd_file to have it active # #location /admin { # See: https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/ #auth_basic \"Administrator's Area\"; #3auth_basic_user_file /path/to/htpasswd_file; #proxy_http_version 1.1; #proxy_set_header Upgrade $http_upgrade; #proxy_set_header Connection $connection_upgrade; #proxy_set_header Host $host; #proxy_set_header X-Real-IP $remote_addr; #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #proxy_set_header X-Forwarded-Proto $scheme; #proxy_pass http://vaultwarden-default; } }","title":"Section 8: Enabling Nginx"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-9-obtain-ssl-certificates","text":"sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d vaultwarden.seattlecommunitynetwork.org Certbot will modify your Nginx configuration to handle HTTPS and redirect from HTTP to HTTPS. Ensure that you copy the file paths provided at the conclusion of the certbot process into the default nginx configuration file, replacing the corresponding comments with these paths.","title":"Section 9: Obtain SSL Certificates"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-10-ensure-everything-is-running","text":"sudo systemctl status nginx","title":"Section 10: Ensure Everything is Running"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-11-additional-features","text":"Enabling admin panel: Go back to /etc/nginx/sites-enabled/default and uncomment the admin section at the bottom. Follow directions at Nginx Admin Guide to encrypt your admin password as a .env file (preferably using argon CLI). Once done, make sure you create a .env file in the directory where the compose file is with VAULTWARDEN_ADMIN_TOKEN=[insert your hashed admin token] . Then in your compose, add these two lines under environment: - ADMIN_TOKEN=${VAULTWARDEN_ADMIN_TOKEN} - DOMAIN=https://vaultwarden.seattlecommunitynetwork.org Restart the container and try logging into https://vaultwarden.seattlecommunitynetwork.org/admin . Once logged in, SMTP and 2FA enabling settings can be configured on the home page.","title":"Section 11: Additional Features"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-12-data-backup","text":"Enabling backups for Vaultwarden is a simple process. Please review the attached backup bash script, which facilitates the transfer of Vaultwarden data to another virtual machine. Additionally, there is a cleanup bash script designed to retain only the most recent file in the other VM, deleting all others. Feel free to modify the scripts as necessary to suit your specific requirements. Backup script: #!/bin/bash docker-compose down datestamp=$(date +%m-%d-%Y) zip -9 -r /home/scn/backups/${datestamp}.zip /opt/vw-data* scp -i ~/.ssh-comm/id_rsa /home/scn/backups/${datestamp}.zip azureuser@[IP address]:~/backups/ docker-compose up -d Cleanup Script: #!/bin/bash # Define the directory containing backup files backup_dir=~/backups # Go to the backup directory cd \"$backup_dir\" || exit # Find and delete older backup files (excluding the latest day) find . -type f -name '*.zip' ! -mtime -1 -exec rm {} + # Exit exit 0","title":"Section 12: Data Backup"},{"location":"infrastructure/proxmox-vaultwarden-deployment/#section-13-using-the-backup","text":"To restore a data backup to the original virtual machine, simply unzip the file and delete the existing contents of /opt/vw-data . Then, transfer the contents of your zip file into this directory. Perform a quick restart of the container, and you will have successfully restored the version of the backup you selected.","title":"Section 13: Using the Backup"},{"location":"infrastructure/sas-setup/","text":"Step 2: eNodeB and SAS Setup Introduction Despite CBRS being a relatively open frequency band, the processes for spectrum access are still somewhat opaque and require significant capital investment and/or ISP-level resources to set up. To clarify this process, here\u2019s a step by step walkthrough tutorial of the setup of a Baicells eNodeB (eNB) base station running in the Citizen\u2019s Broadband Radio Service (CBRS) spectrum band (or band 48). Before following this tutorial, you should have completed the setup of a LTE Evolved Packet Core (EPC) to control your eNB, for which the setup of an open source version based on open5gs is outlined in this tutorial . I. Get set up with a Spectrum Access System (SAS) A. Why get set up with a SAS? Current FCC regulations require all CBRS equipment (called a CBSD) to be registered on a Spectrum Access System (SAS) that coordinates all spectrum assignments and ensures that no transmissions interfere with each other. This will likely require a commercial agreement with a SAS provider such as Google, Federated Wireless, etc. This tutorial uses the Google SAS. B. CPI License At least one member of your team will require \u201cCertified Professional Installer\u201d (CPI) training and license in order to hold legal responsibility for and sign off on device installations. Most SAS providers will offer training at about $500 for both an online training course and the certification exam. If you aren\u2019t able to get someone on your team certified, be sure to collaborate with a CPI! Feel free to contact us at the Local Connectivity Lab if you need support for your community project in this regard, and we can figure out what is feasible. The following are some links and helpful notes about this process: * https://wifidevan.wordpress.com/cbrs-certified-professional-installer-cpi-study-notes/ * https://alliancecorporation.ca/webinars/webinars-webinars/cbrs-for-beginners-part-2-by-commscope/ * https://cbrs.wirelessinnovation.org/acronyms C. SAS Pricing Agreements For Google, the price options provided us in summer 2020 were: Fixed Wireless SAS services are billed per link/household so you pay for each CPE (Customer Premises Equipment) CBSD registered with SAS. CBSDs that operate as base stations are free of charge. Price Per Customer Link $2.25/month. Mobility/Private LTE (price is based on CBSD categoris) Category A CBSD max transmit capability: 30 dBm/10 MHz = 20 dBm/MHz or \u201c1 Watt\u201d mounted under 6m Height Above Average Terrain measured 3-16 km away from site $2.67/month Category B CBSD max transmit capability: Maximum EIRP of 47 dBm/10 MHz = 37 dBm/MHz or \u201c50 Watt\" $13.33/month. D. SAS Registration CBSDs must register their transmit capabilities with the SAS using either the \u201cone-step\u201d or \u201cmulti-step\u201d process. The one-step process requires you to input all installation parameters and sign them with the CPI certificate all on the base station itself, or via a cloud domain proxy such as used by Baicells. Not all base stations support this and the interfaces for doing so might vary widely, so \u201cmulti-step\u201d is typically recommended. II. Register device in SAS portal This tutorial will be walking through steps following the specifics of the Google SAS portal interface, but the steps should be generalizable to other SAS portals. A. Once you have an account on an SAS service, register your devices on their portal or dashboard. The Google SAS portal can be found at: https://wirelessconnectivity.google.com/sas/ B. Our Setup Our test setup in the lab includes: 1W Baicells Nova 233 base station in the CBRS band mounted on the 6th floor balcony of our UW computer science building. Alpha Wireless 18 dBi-gain panel antenna with a beamwidth of 65 degrees (model AW3014-T4), mounted straight ahead and not tilted down. C. Example Configuration An example configuration for this setup is shown below. The configuration screen is a right-hand sidebar next to the map view, hence the unwieldy aspect ratio. Explanation of parameters: CBSD Category (A or B): Defined by rules in Section I.C above User ID Specified by the SAS provider when you register FCC ID and Serial Number: Both the radio and antenna model must be pre-authorized for use with CBRS by the FCC. The FCC ID is used to identify this approved device type. The serial number specifies the exact device identity. Both can usually be found on the outside of the device (circled in image below). Beamforming Gain, Beamwidth Based on antenna specs in II.B EIRP Effective Isotropic Radiated Power of your system including both the base station radio and antenna. For a Cat B CBSD, this must be 46 dBm/10 MHz=36 dBm/MHz or lower. Calculate this value by adding the max transmit power (actually power density per MHz) of the base station, in our case 28 dBm, to the antenna beamforming gain, in our case 18 dBi; 28+18=36 dBm/MHz. For the units requested by the Google interface, add 10 to this value to specify power per 10 MHz instead of per MHz. Height Specified in terms of height Above Ground Level (AGL) which you can measure using a rangefinder/ measuring tape/ building plan, or in height Above Mean Sea Level (AMSL). Not in terms of HAAT as in the Cat A/B definition. Must be accurate to within 3 m. Azimuth Refers to the compass heading/ direction that the antenna is pointing (set this to 0 for an omnidirectional antenna). This FCC tool is extremely helpful for calculating the azimuth based on the antenna\u2019s gps location and that of a structure you are pointing it at. You can get these GPS coordinates via Google Maps or Google Earth. Air Interface E_UTRA is the LTE radio standard used by our Baicells box. The only \u201csupported spec\u201d currently available for Baicells is FFS (according to a forum post, linked here). Location: In the Google interface, set the site location in GPS coordinates under the tab labeled with the map pin icon. (not shown) Parameters under \"CBSD Info\" Call Sign As far as I can tell, this can be any reasonable alphanumeric string as long as it is unique and matches the value of the \u201ccall sign\u201d parameter as sent over by the eNB or domain proxy. You will set this in the SAS interface as well as either the eNB or Baicells Cloud Core (they all need to match). Others These should match the settings with the same name on the eNB\u2019s local management portal, shown on the \u201cBasic Info\u201d page in section IV.A below. D. CPI Signature When the parameters are all filled out, click the big red \u201cReady for CPI\u201d button at the bottom of the panel (not shown here). On the CPI\u2019s version of the interface, it will provide a place to \u201csign\u201d the configuration with their CPI certificate, which they will upload to the interface. This must happen before the device can get a spectrum grant. E. Status Tab After the CPI signs the eNB configuration, under the \u201cStatus\u201d tab (visible in the config panel), you should see \u201cNot yet Registered\u201d (or a similar message) because the eNB has not checked in to the Google SAS yet with its matching parameters to complete the multi-step process. If something has otherwise gone wrong, you\u2019ll see an error message here. F. Other helpful links Google CBSD registration and deregistration Elevation finder tool with map III. Steps in Baicells Cloud interface A. Make a Baicells OMC account. Due to Baicells\u2019 use of a \u201cdomain proxy\u201d for their SAS requests, you will need to make a new user account in the Baicells Operators Management Console (OMC): https://cloudcore.baicells.com:4443/ This is distinct from their paid \u201cCloud Core\u201d service which we will not be using in this tutorial, although the management portal is the same. B. Take note of the CloudKey Once you have made an account, note the 6-letter \u201cCloudKey\u201d in the upper right corner of the screen (circled in red). This will need to be inputted into the local eNB management portal for the eNB to check into the Cloud OMC. On your version of this portal, if you\u2019re doing this for the first time, you shouldn\u2019t see any eNBs already present. C. Set your SAS service provider. Navigate to Advance\u2192SAS in the left hand menu, and then click the gear icon on the upper right corner, which has the hover text \u201cSettings.\u201d IV. Steps in Baicells management interface A. Local Management Portal The Baicells eNodeB (eNB) is best managed through the browser-based management portal; the current command line interface is accessible but extremely limited. The default IP address of the management portal (and that of most Baicells equipment I\u2019ve seen) is 192.168.150.1, and the default login credentials are admin/admin. I would recommend changing the admin login credentials to be more secure. Connect your computer to the eNB via Ethernet, and navigate to this IP address in your browser (using http://192.168.150.1, not https). Baicells Initial Login Screen: BTS Info\u2192\u201cBasic Info\u201d Page visible upon login: B. Upgrade firmware Upgrade the firmware to the latest firmware version that supports SAS functionality, or verify that it is already up to date. You can check the official firmware page under the correct eNB model. The Nova 233 CBRS small cell we\u2019re using is model mBS1105. The latest firmware version after which SAS is officially supported is BaiBS_RTS_3.6.6.IMG (as of Feb 2021), for which the direct download is available here . Do not skip this step, otherwise none of the following steps will work right. C. Get everything connected Once the firmware is upgraded, you will want to get the eNB connected to your local LTE core network (EPC) as well as to the Internet so it can contact the necessary SAS infrastructure. 1. Configure Internet Access (WAN) Navigate to the Network\u2192WAN/LAN/VLAN tab on the left hand menu. We will set the WAN interface IP address to 192.168.151.1, since the Baicells console requires (for whatever reason) a different subnet for the WAN as opposed to the LAN. Then we will connect the eNB to an Ethernet port on the EPC that has the IP address 192.168.151.2 (as set up in our previous tutorial), which will act as the eNB\u2019s Internet gateway. Don\u2019t forget to hit \u201cSave\u201d after each change you make in this interface. 2. Check Internet access At this point, if the EPC is configured correctly to pass eNB traffic to the Internet, the eNB should be able to ping an arbitrary IP address. To test this, navigate to the Network\u2192Diagnostics tab on the left hand menu and select \u201cPing\u201d under the \u201cMethod of Diagnostics\u201d dropdown menu. Set the \u201cTarget IP Domain\u201d to be a highly reachable IP address on the Internet such as 1.1.1.1, which is the CloudFlare DNS server. Press \u201cImplement.\u201d If the result is \u201cFail!\u201d as in the screenshot, there is likely something wrong with your eNB\u2019s Internet connection through the EPC; you should fix this issue before continuing. 3. Reboot as needed If a message appears that the eNB needs a reboot after the new settings are saved, navigate to the Reboot tab in the left hand menu and perform the reboot (Warm Reset is fine). 4. Attach to Baicells OMC To configure the eNB to talk to the OMC as discussed in the prior section, navigate to the BTS Setting\u2192Management Server tab in the management console and enter the CloudKey. Within a few minutes, the eNB should appear in your Baicells Cloud OMC console, and the \u201cBasic Info\u201d page should show that the OMC is \u201cConnected.\u201d 5. Disable IPsec For our purposes we will not be using IPsec between our EPC and eNB; the default IPSec configured is used for the Baicells Cloud EPC which we are not using. Navigate to the Network\u2192\u201cMME&IPSec Binding\u201d menu tab and set \u201cIPSec Status\u201d to \u201cDisable.\u201d You may also delete the IPSec tunnels as shown below. 6. Disable GPS Sync when testing indoors. Navigate to the \u201cBTS Setting\u201d\u2192\u201cSync Setting\u201d menu and disable both \u201cForced Sync\u201d and \u201cGPS Sync Switch,\u201d in case you need to work with the base station in a location where you don\u2019t have a strong GPS signal. Some base stations will not start up normally or attach to the EPC unless they get a GPS signal, and we should avoid this behavior. 7. Change the MME settings Change the MME settings. Since we are using our local EPC, we will need to change the MME settings to reflect our MME\u2019s IP address, on which it is listening for eNBs to attach, as well as other configurations. Navigate to the BTS Info\u2192Quick Setting tab on the left hand menu. Disable RF You should set the \u201cRF Status\u201d setting to \u201cDisable\u201d before you change the MME IP, because attaching to the MME will normally cause the eNB\u2019s radio to turn on. Since we have not enabled the eNB to ask for spectrum coordinated by the SAS yet, turning on the radio may cause unwanted interference on someone else\u2019s network. PLMN setting Remove the existing \u201cPLMN ID\u201d (by clicking the trash can symbol) and set it to the value that you have configured in your EPC. In our networks, we use \u201c91054\u201d as our PLMN, so add this as a \u201cPrimary\u201d and \u201cNotReserved\u201d PLMN by entering the number in the text box and clicking the \u201c+\u201d button. MME IP address Remove the existing MME IP associated with the old PLMN. Add the new MME IP address, in our case 192.168.150.2, by entering it in the text box and clicking \u201c+\u201d. This MME IP should be associated with the newly added PLMN by default. Save the changes and reboot the eNB (Warm Reset); after the reboot has finished (within a few minutes), the eNB should attach to the MME. If you navigate to BTS Info\u2192Basic Info, you should see the MME Status change from \u201cNot Connected\u201d to \u201cConnected.\u201d If you are looking at the MME logs on the EPC, you will also see the record that an eNB has attached. 8. Enable SAS SAS should only be enabled after successfully attaching the eNB to the MME. Unfortunately, when SAS is enabled, the eNB will not attach to the MME unless it has a currently valid authorization to transmit on a certain frequency. However, until it is attached to an MME, the Baicells Cloud OMC will not provide it this authorization. So we need to have SAS disabled first with the RF also disabled, attach the eNB to the MME, and then enable SAS. Choose \u201cMulti-step\u201d under \u201cSAS Registration Type,\u201d as specified in Section I.E. Also choose \u201cB\u201d under \u201ccategory,\u201d and write in the other parameters to match the ones with the same name in the Google SAS configuration. After you click \u201cSave,\u201d SAS should be enabled immediately. You should see the SAS enabled status change in the Baicells Cloud OMC. If all goes smoothly, your device should get an authorization to transmit within a few minutes and the radio should turn on! 9. Check Baicells CLOUD OMC to debug issues You can check the status of the SAS authorization process in the Cloud OMC. Here you can find logs (upper right corner of SAS screen, shown in the screenshot below) with any error messages that may have occurred in the process. Errors can be caused by invalid or non-matching parameter values, lack of CPI signature, lack of spectrum availability, etc. In more difficult cases, after device registration the SAS may not respond to spectrum inquiries without sending any clear error messages. I have encountered this scenario when requesting spectrum around midnight, which may have been caused by brief database unavailability during the daily \u201cSAS Sync\u201d or IAP. My recommendation is to avoid requesting a new spectrum grant after 11 pm PST. If you change anything about the equipment used on site or the location/orientation of the equipment, you need to change the SAS registration, have it re-signed by the CPI, and use the Baicells OMC to re-request a new spectrum authorization- this process is described in the following section. V. How to change location, antenna properties, etc. after deployment As an example, this section will show how you would change the equipment\u2019s location upon moving from test site to deployment site. Get the new GPS location either manually using Google Maps/Earth, or automatically using Baicells OMC\u2019s GPS reading for the eNB if available. Google SAS steps In the upper right hand corner of the Google SAS configuration for the deployed equipment (long narrow right side panel for a particular site), press the unlock button (shaped like a padlock) to make the configuration editable. To edit the site location, click on the map pin icon in the upper left corner of this same right hand configuration panel to enter the location panel. Enter the new GPS coordinates in the box. After your changes, lock the site configuration again. (If the red \u201cReady for CPI\u201d button appears again at the bottom of the main configuration panel, go ahead and click it to prompt the CPI to sign.) You may have to wait a few minutes or hours for the changes to sync to the CPI\u2019s SAS database view. If after a while the CPI still cannot see the location change, ask them to enter the new GPS coordinates in their own interface and re-sign the configuration. Baicells Cloud OMC steps On the Baicells OMC, navigate to the Advance\u2192SAS screen where you can see the list of CBRS devices and their SAS status. Click on the 3 dots ( \u2807) symbol before the serial number for a particular device and click on \u201cProcedure\u201d to enter the SAS procedure screen. On the Procedure screen, you can see the most recent SAS logs, relinquish and re-request active spectrum authorizations, or de-register and re-register devices. First click on the \u201cAuthorized\u201d icon and click on the \u201cRelinquishment req\u201d button to relinquish the current spectrum authorization. Then the latter two icons will become greyed out, but the device will remain registered. We will need to fully de-register and re-register the device with the new parameters. Click the \u201cRegistered\u201d icon and then the \u201cDe-register\u201d button when it appears to de-register the device. Once the device is in the \u201cUnregistered\u201d state, click the \u201cUnregistered\u201d icon and then click the \u201cRegister req\u201d button when it appears. If all goes well, the device should re-register, and also request and receive a new grant (completing the full procedure) within a few moments.","title":"Step 2. eNodeB and SAS Setup"},{"location":"infrastructure/sas-setup/#step-2-enodeb-and-sas-setup","text":"","title":"Step 2: eNodeB and SAS Setup"},{"location":"infrastructure/sas-setup/#introduction","text":"Despite CBRS being a relatively open frequency band, the processes for spectrum access are still somewhat opaque and require significant capital investment and/or ISP-level resources to set up. To clarify this process, here\u2019s a step by step walkthrough tutorial of the setup of a Baicells eNodeB (eNB) base station running in the Citizen\u2019s Broadband Radio Service (CBRS) spectrum band (or band 48). Before following this tutorial, you should have completed the setup of a LTE Evolved Packet Core (EPC) to control your eNB, for which the setup of an open source version based on open5gs is outlined in this tutorial .","title":"Introduction"},{"location":"infrastructure/sas-setup/#i-get-set-up-with-a-spectrum-access-system-sas","text":"","title":"I. Get set up with a Spectrum Access System (SAS)"},{"location":"infrastructure/sas-setup/#a-why-get-set-up-with-a-sas","text":"Current FCC regulations require all CBRS equipment (called a CBSD) to be registered on a Spectrum Access System (SAS) that coordinates all spectrum assignments and ensures that no transmissions interfere with each other. This will likely require a commercial agreement with a SAS provider such as Google, Federated Wireless, etc. This tutorial uses the Google SAS.","title":"A. Why get set up with a SAS?"},{"location":"infrastructure/sas-setup/#b-cpi-license","text":"At least one member of your team will require \u201cCertified Professional Installer\u201d (CPI) training and license in order to hold legal responsibility for and sign off on device installations. Most SAS providers will offer training at about $500 for both an online training course and the certification exam. If you aren\u2019t able to get someone on your team certified, be sure to collaborate with a CPI! Feel free to contact us at the Local Connectivity Lab if you need support for your community project in this regard, and we can figure out what is feasible. The following are some links and helpful notes about this process: * https://wifidevan.wordpress.com/cbrs-certified-professional-installer-cpi-study-notes/ * https://alliancecorporation.ca/webinars/webinars-webinars/cbrs-for-beginners-part-2-by-commscope/ * https://cbrs.wirelessinnovation.org/acronyms","title":"B. CPI License"},{"location":"infrastructure/sas-setup/#c-sas-pricing-agreements","text":"For Google, the price options provided us in summer 2020 were: Fixed Wireless SAS services are billed per link/household so you pay for each CPE (Customer Premises Equipment) CBSD registered with SAS. CBSDs that operate as base stations are free of charge. Price Per Customer Link $2.25/month. Mobility/Private LTE (price is based on CBSD categoris) Category A CBSD max transmit capability: 30 dBm/10 MHz = 20 dBm/MHz or \u201c1 Watt\u201d mounted under 6m Height Above Average Terrain measured 3-16 km away from site $2.67/month Category B CBSD max transmit capability: Maximum EIRP of 47 dBm/10 MHz = 37 dBm/MHz or \u201c50 Watt\" $13.33/month.","title":"C. SAS Pricing Agreements"},{"location":"infrastructure/sas-setup/#d-sas-registration","text":"CBSDs must register their transmit capabilities with the SAS using either the \u201cone-step\u201d or \u201cmulti-step\u201d process. The one-step process requires you to input all installation parameters and sign them with the CPI certificate all on the base station itself, or via a cloud domain proxy such as used by Baicells. Not all base stations support this and the interfaces for doing so might vary widely, so \u201cmulti-step\u201d is typically recommended.","title":"D. SAS Registration"},{"location":"infrastructure/sas-setup/#ii-register-device-in-sas-portal","text":"This tutorial will be walking through steps following the specifics of the Google SAS portal interface, but the steps should be generalizable to other SAS portals.","title":"II. Register device in SAS portal"},{"location":"infrastructure/sas-setup/#a-once-you-have-an-account-on-an-sas-service-register-your-devices-on-their-portal-or-dashboard","text":"The Google SAS portal can be found at: https://wirelessconnectivity.google.com/sas/","title":"A. Once you have an account on an SAS service, register your devices on their portal or dashboard."},{"location":"infrastructure/sas-setup/#b-our-setup","text":"Our test setup in the lab includes: 1W Baicells Nova 233 base station in the CBRS band mounted on the 6th floor balcony of our UW computer science building. Alpha Wireless 18 dBi-gain panel antenna with a beamwidth of 65 degrees (model AW3014-T4), mounted straight ahead and not tilted down.","title":"B. Our Setup"},{"location":"infrastructure/sas-setup/#c-example-configuration","text":"An example configuration for this setup is shown below. The configuration screen is a right-hand sidebar next to the map view, hence the unwieldy aspect ratio. Explanation of parameters: CBSD Category (A or B): Defined by rules in Section I.C above User ID Specified by the SAS provider when you register FCC ID and Serial Number: Both the radio and antenna model must be pre-authorized for use with CBRS by the FCC. The FCC ID is used to identify this approved device type. The serial number specifies the exact device identity. Both can usually be found on the outside of the device (circled in image below). Beamforming Gain, Beamwidth Based on antenna specs in II.B EIRP Effective Isotropic Radiated Power of your system including both the base station radio and antenna. For a Cat B CBSD, this must be 46 dBm/10 MHz=36 dBm/MHz or lower. Calculate this value by adding the max transmit power (actually power density per MHz) of the base station, in our case 28 dBm, to the antenna beamforming gain, in our case 18 dBi; 28+18=36 dBm/MHz. For the units requested by the Google interface, add 10 to this value to specify power per 10 MHz instead of per MHz. Height Specified in terms of height Above Ground Level (AGL) which you can measure using a rangefinder/ measuring tape/ building plan, or in height Above Mean Sea Level (AMSL). Not in terms of HAAT as in the Cat A/B definition. Must be accurate to within 3 m. Azimuth Refers to the compass heading/ direction that the antenna is pointing (set this to 0 for an omnidirectional antenna). This FCC tool is extremely helpful for calculating the azimuth based on the antenna\u2019s gps location and that of a structure you are pointing it at. You can get these GPS coordinates via Google Maps or Google Earth. Air Interface E_UTRA is the LTE radio standard used by our Baicells box. The only \u201csupported spec\u201d currently available for Baicells is FFS (according to a forum post, linked here). Location: In the Google interface, set the site location in GPS coordinates under the tab labeled with the map pin icon. (not shown) Parameters under \"CBSD Info\" Call Sign As far as I can tell, this can be any reasonable alphanumeric string as long as it is unique and matches the value of the \u201ccall sign\u201d parameter as sent over by the eNB or domain proxy. You will set this in the SAS interface as well as either the eNB or Baicells Cloud Core (they all need to match). Others These should match the settings with the same name on the eNB\u2019s local management portal, shown on the \u201cBasic Info\u201d page in section IV.A below.","title":"C. Example Configuration"},{"location":"infrastructure/sas-setup/#d-cpi-signature","text":"When the parameters are all filled out, click the big red \u201cReady for CPI\u201d button at the bottom of the panel (not shown here). On the CPI\u2019s version of the interface, it will provide a place to \u201csign\u201d the configuration with their CPI certificate, which they will upload to the interface. This must happen before the device can get a spectrum grant.","title":"D. CPI Signature"},{"location":"infrastructure/sas-setup/#e-status-tab","text":"After the CPI signs the eNB configuration, under the \u201cStatus\u201d tab (visible in the config panel), you should see \u201cNot yet Registered\u201d (or a similar message) because the eNB has not checked in to the Google SAS yet with its matching parameters to complete the multi-step process. If something has otherwise gone wrong, you\u2019ll see an error message here.","title":"E. Status Tab"},{"location":"infrastructure/sas-setup/#f-other-helpful-links","text":"Google CBSD registration and deregistration Elevation finder tool with map","title":"F. Other helpful links"},{"location":"infrastructure/sas-setup/#iii-steps-in-baicells-cloud-interface","text":"","title":"III. Steps in Baicells Cloud interface"},{"location":"infrastructure/sas-setup/#a-make-a-baicells-omc-account","text":"Due to Baicells\u2019 use of a \u201cdomain proxy\u201d for their SAS requests, you will need to make a new user account in the Baicells Operators Management Console (OMC): https://cloudcore.baicells.com:4443/ This is distinct from their paid \u201cCloud Core\u201d service which we will not be using in this tutorial, although the management portal is the same.","title":"A. Make a Baicells OMC account."},{"location":"infrastructure/sas-setup/#b-take-note-of-the-cloudkey","text":"Once you have made an account, note the 6-letter \u201cCloudKey\u201d in the upper right corner of the screen (circled in red). This will need to be inputted into the local eNB management portal for the eNB to check into the Cloud OMC. On your version of this portal, if you\u2019re doing this for the first time, you shouldn\u2019t see any eNBs already present.","title":"B. Take note of the CloudKey"},{"location":"infrastructure/sas-setup/#c-set-your-sas-service-provider","text":"Navigate to Advance\u2192SAS in the left hand menu, and then click the gear icon on the upper right corner, which has the hover text \u201cSettings.\u201d","title":"C. Set your SAS service provider."},{"location":"infrastructure/sas-setup/#iv-steps-in-baicells-management-interface","text":"","title":"IV. Steps in Baicells management interface"},{"location":"infrastructure/sas-setup/#a-local-management-portal","text":"The Baicells eNodeB (eNB) is best managed through the browser-based management portal; the current command line interface is accessible but extremely limited. The default IP address of the management portal (and that of most Baicells equipment I\u2019ve seen) is 192.168.150.1, and the default login credentials are admin/admin. I would recommend changing the admin login credentials to be more secure. Connect your computer to the eNB via Ethernet, and navigate to this IP address in your browser (using http://192.168.150.1, not https). Baicells Initial Login Screen: BTS Info\u2192\u201cBasic Info\u201d Page visible upon login:","title":"A. Local Management Portal"},{"location":"infrastructure/sas-setup/#b-upgrade-firmware","text":"Upgrade the firmware to the latest firmware version that supports SAS functionality, or verify that it is already up to date. You can check the official firmware page under the correct eNB model. The Nova 233 CBRS small cell we\u2019re using is model mBS1105. The latest firmware version after which SAS is officially supported is BaiBS_RTS_3.6.6.IMG (as of Feb 2021), for which the direct download is available here . Do not skip this step, otherwise none of the following steps will work right.","title":"B. Upgrade firmware"},{"location":"infrastructure/sas-setup/#c-get-everything-connected","text":"Once the firmware is upgraded, you will want to get the eNB connected to your local LTE core network (EPC) as well as to the Internet so it can contact the necessary SAS infrastructure.","title":"C. Get everything connected"},{"location":"infrastructure/sas-setup/#1-configure-internet-access-wan","text":"Navigate to the Network\u2192WAN/LAN/VLAN tab on the left hand menu. We will set the WAN interface IP address to 192.168.151.1, since the Baicells console requires (for whatever reason) a different subnet for the WAN as opposed to the LAN. Then we will connect the eNB to an Ethernet port on the EPC that has the IP address 192.168.151.2 (as set up in our previous tutorial), which will act as the eNB\u2019s Internet gateway. Don\u2019t forget to hit \u201cSave\u201d after each change you make in this interface.","title":"1. Configure Internet Access (WAN)"},{"location":"infrastructure/sas-setup/#2-check-internet-access","text":"At this point, if the EPC is configured correctly to pass eNB traffic to the Internet, the eNB should be able to ping an arbitrary IP address. To test this, navigate to the Network\u2192Diagnostics tab on the left hand menu and select \u201cPing\u201d under the \u201cMethod of Diagnostics\u201d dropdown menu. Set the \u201cTarget IP Domain\u201d to be a highly reachable IP address on the Internet such as 1.1.1.1, which is the CloudFlare DNS server. Press \u201cImplement.\u201d If the result is \u201cFail!\u201d as in the screenshot, there is likely something wrong with your eNB\u2019s Internet connection through the EPC; you should fix this issue before continuing.","title":"2. Check Internet access"},{"location":"infrastructure/sas-setup/#3-reboot-as-needed","text":"If a message appears that the eNB needs a reboot after the new settings are saved, navigate to the Reboot tab in the left hand menu and perform the reboot (Warm Reset is fine).","title":"3. Reboot as needed"},{"location":"infrastructure/sas-setup/#4-attach-to-baicells-omc","text":"To configure the eNB to talk to the OMC as discussed in the prior section, navigate to the BTS Setting\u2192Management Server tab in the management console and enter the CloudKey. Within a few minutes, the eNB should appear in your Baicells Cloud OMC console, and the \u201cBasic Info\u201d page should show that the OMC is \u201cConnected.\u201d","title":"4. Attach to Baicells OMC"},{"location":"infrastructure/sas-setup/#5-disable-ipsec","text":"For our purposes we will not be using IPsec between our EPC and eNB; the default IPSec configured is used for the Baicells Cloud EPC which we are not using. Navigate to the Network\u2192\u201cMME&IPSec Binding\u201d menu tab and set \u201cIPSec Status\u201d to \u201cDisable.\u201d You may also delete the IPSec tunnels as shown below.","title":"5. Disable IPsec"},{"location":"infrastructure/sas-setup/#6-disable-gps-sync-when-testing-indoors","text":"Navigate to the \u201cBTS Setting\u201d\u2192\u201cSync Setting\u201d menu and disable both \u201cForced Sync\u201d and \u201cGPS Sync Switch,\u201d in case you need to work with the base station in a location where you don\u2019t have a strong GPS signal. Some base stations will not start up normally or attach to the EPC unless they get a GPS signal, and we should avoid this behavior.","title":"6. Disable GPS Sync when testing indoors."},{"location":"infrastructure/sas-setup/#7-change-the-mme-settings","text":"Change the MME settings. Since we are using our local EPC, we will need to change the MME settings to reflect our MME\u2019s IP address, on which it is listening for eNBs to attach, as well as other configurations. Navigate to the BTS Info\u2192Quick Setting tab on the left hand menu. Disable RF You should set the \u201cRF Status\u201d setting to \u201cDisable\u201d before you change the MME IP, because attaching to the MME will normally cause the eNB\u2019s radio to turn on. Since we have not enabled the eNB to ask for spectrum coordinated by the SAS yet, turning on the radio may cause unwanted interference on someone else\u2019s network. PLMN setting Remove the existing \u201cPLMN ID\u201d (by clicking the trash can symbol) and set it to the value that you have configured in your EPC. In our networks, we use \u201c91054\u201d as our PLMN, so add this as a \u201cPrimary\u201d and \u201cNotReserved\u201d PLMN by entering the number in the text box and clicking the \u201c+\u201d button. MME IP address Remove the existing MME IP associated with the old PLMN. Add the new MME IP address, in our case 192.168.150.2, by entering it in the text box and clicking \u201c+\u201d. This MME IP should be associated with the newly added PLMN by default. Save the changes and reboot the eNB (Warm Reset); after the reboot has finished (within a few minutes), the eNB should attach to the MME. If you navigate to BTS Info\u2192Basic Info, you should see the MME Status change from \u201cNot Connected\u201d to \u201cConnected.\u201d If you are looking at the MME logs on the EPC, you will also see the record that an eNB has attached.","title":"7. Change the MME settings"},{"location":"infrastructure/sas-setup/#8-enable-sas","text":"SAS should only be enabled after successfully attaching the eNB to the MME. Unfortunately, when SAS is enabled, the eNB will not attach to the MME unless it has a currently valid authorization to transmit on a certain frequency. However, until it is attached to an MME, the Baicells Cloud OMC will not provide it this authorization. So we need to have SAS disabled first with the RF also disabled, attach the eNB to the MME, and then enable SAS. Choose \u201cMulti-step\u201d under \u201cSAS Registration Type,\u201d as specified in Section I.E. Also choose \u201cB\u201d under \u201ccategory,\u201d and write in the other parameters to match the ones with the same name in the Google SAS configuration. After you click \u201cSave,\u201d SAS should be enabled immediately. You should see the SAS enabled status change in the Baicells Cloud OMC. If all goes smoothly, your device should get an authorization to transmit within a few minutes and the radio should turn on!","title":"8. Enable SAS"},{"location":"infrastructure/sas-setup/#9-check-baicells-cloud-omc-to-debug-issues","text":"You can check the status of the SAS authorization process in the Cloud OMC. Here you can find logs (upper right corner of SAS screen, shown in the screenshot below) with any error messages that may have occurred in the process. Errors can be caused by invalid or non-matching parameter values, lack of CPI signature, lack of spectrum availability, etc. In more difficult cases, after device registration the SAS may not respond to spectrum inquiries without sending any clear error messages. I have encountered this scenario when requesting spectrum around midnight, which may have been caused by brief database unavailability during the daily \u201cSAS Sync\u201d or IAP. My recommendation is to avoid requesting a new spectrum grant after 11 pm PST. If you change anything about the equipment used on site or the location/orientation of the equipment, you need to change the SAS registration, have it re-signed by the CPI, and use the Baicells OMC to re-request a new spectrum authorization- this process is described in the following section.","title":"9. Check Baicells CLOUD OMC to debug issues"},{"location":"infrastructure/sas-setup/#v-how-to-change-location-antenna-properties-etc-after-deployment","text":"As an example, this section will show how you would change the equipment\u2019s location upon moving from test site to deployment site. Get the new GPS location either manually using Google Maps/Earth, or automatically using Baicells OMC\u2019s GPS reading for the eNB if available. Google SAS steps In the upper right hand corner of the Google SAS configuration for the deployed equipment (long narrow right side panel for a particular site), press the unlock button (shaped like a padlock) to make the configuration editable. To edit the site location, click on the map pin icon in the upper left corner of this same right hand configuration panel to enter the location panel. Enter the new GPS coordinates in the box. After your changes, lock the site configuration again. (If the red \u201cReady for CPI\u201d button appears again at the bottom of the main configuration panel, go ahead and click it to prompt the CPI to sign.) You may have to wait a few minutes or hours for the changes to sync to the CPI\u2019s SAS database view. If after a while the CPI still cannot see the location change, ask them to enter the new GPS coordinates in their own interface and re-sign the configuration. Baicells Cloud OMC steps On the Baicells OMC, navigate to the Advance\u2192SAS screen where you can see the list of CBRS devices and their SAS status. Click on the 3 dots ( \u2807) symbol before the serial number for a particular device and click on \u201cProcedure\u201d to enter the SAS procedure screen. On the Procedure screen, you can see the most recent SAS logs, relinquish and re-request active spectrum authorizations, or de-register and re-register devices. First click on the \u201cAuthorized\u201d icon and click on the \u201cRelinquishment req\u201d button to relinquish the current spectrum authorization. Then the latter two icons will become greyed out, but the device will remain registered. We will need to fully de-register and re-register the device with the new parameters. Click the \u201cRegistered\u201d icon and then the \u201cDe-register\u201d button when it appears to de-register the device. Once the device is in the \u201cUnregistered\u201d state, click the \u201cUnregistered\u201d icon and then click the \u201cRegister req\u201d button when it appears. If all goes well, the device should re-register, and also request and receive a new grant (completing the full procedure) within a few moments.","title":"V. How to change location, antenna properties, etc. after deployment"},{"location":"infrastructure/software/","text":"Our Software Here is a list of the software that we use to deploy, maintain, and plan our network sites. Networking Local Services We use the CoLTE project maintained by the University of Washington ICTD Lab to provide services such as network monitoring, web-based administration, and local web and DNS serving/caching. Evolved Packet Core (EPC) Our EPC is powered by Open5GS , an open-source project for 4G and 5G core networks. Currently all of our networks are 4G networks. Spectrum Access System (SAS) We have a partnership with Google SAS to gain access to CBRS spectrum. Learn more about our SAS setup here . Network Monitoring and Alerting We use LibreNMS and SNMPd to monitor our nodes and provide alerting. Our Baicells-specific Network Manager setup is documented here , and our instructions for configuring a new node can be found here . Field Measurement Network Performance Measurement Tool The LCL Network Performance Measurement Tool is an Android App in development that will measure a variety of network metrics, including but not limited to ping, upload/download speed, signal strength. We will use this tool to easily capture and upload network metrics in the field so that we can provide better estimates of what kind of Internet access that our users can expect to receive. Network Cell Info Lite Network Cell Info Lite is an Android App on the Google Playstore that is free to use (with advertisements) and is capable of taking network metric measurements and recording them to upload. This is an option that we use but are not satisfied with for many reasons, which is why we are developing our own app. Site Planning Google Earth We primarily use the Google Earth Pro desktop application to do a rough line-of-sight evaluation. We perform what is called a \"viewshed analysis\" that allows us to determine what is visible from a specific point on Earth (e.g. a rooftop). Ubiquiti Line of Sight A web-based line of sight tool provided by Ubiquiti that contains helpful altitude data and diagrams. A drawback is that it is specialized to provide data for Ubiquiti devices only. Other resources Facebook ISP Toolbox Line of Sight A web-based line of sight tool provided by Facebook Connectivity that utilizes public LiDAR data. Unfortunately LiDAR data for the Seattle area is not present yet, although there is data for some areas in Tacoma. Facebook ISP Toolbox Market Evaluator A web-based market evaluator provided by Facebook Connectivity that can be used to provide more context about the areas around potential network sites. It offers information about other service providers in the area, average household income, median speeds, and current lowest broadband price available.","title":"Software Overview"},{"location":"infrastructure/software/#our-software","text":"Here is a list of the software that we use to deploy, maintain, and plan our network sites.","title":"Our Software"},{"location":"infrastructure/software/#networking","text":"","title":"Networking"},{"location":"infrastructure/software/#local-services","text":"We use the CoLTE project maintained by the University of Washington ICTD Lab to provide services such as network monitoring, web-based administration, and local web and DNS serving/caching.","title":"Local Services"},{"location":"infrastructure/software/#evolved-packet-core-epc","text":"Our EPC is powered by Open5GS , an open-source project for 4G and 5G core networks. Currently all of our networks are 4G networks.","title":"Evolved Packet Core (EPC)"},{"location":"infrastructure/software/#spectrum-access-system-sas","text":"We have a partnership with Google SAS to gain access to CBRS spectrum. Learn more about our SAS setup here .","title":"Spectrum Access System (SAS)"},{"location":"infrastructure/software/#network-monitoring-and-alerting","text":"We use LibreNMS and SNMPd to monitor our nodes and provide alerting. Our Baicells-specific Network Manager setup is documented here , and our instructions for configuring a new node can be found here .","title":"Network Monitoring and Alerting"},{"location":"infrastructure/software/#field-measurement","text":"","title":"Field Measurement"},{"location":"infrastructure/software/#network-performance-measurement-tool","text":"The LCL Network Performance Measurement Tool is an Android App in development that will measure a variety of network metrics, including but not limited to ping, upload/download speed, signal strength. We will use this tool to easily capture and upload network metrics in the field so that we can provide better estimates of what kind of Internet access that our users can expect to receive.","title":"Network Performance Measurement Tool"},{"location":"infrastructure/software/#network-cell-info-lite","text":"Network Cell Info Lite is an Android App on the Google Playstore that is free to use (with advertisements) and is capable of taking network metric measurements and recording them to upload. This is an option that we use but are not satisfied with for many reasons, which is why we are developing our own app.","title":"Network Cell Info Lite"},{"location":"infrastructure/software/#site-planning","text":"","title":"Site Planning"},{"location":"infrastructure/software/#google-earth","text":"We primarily use the Google Earth Pro desktop application to do a rough line-of-sight evaluation. We perform what is called a \"viewshed analysis\" that allows us to determine what is visible from a specific point on Earth (e.g. a rooftop).","title":"Google Earth"},{"location":"infrastructure/software/#ubiquiti-line-of-sight","text":"A web-based line of sight tool provided by Ubiquiti that contains helpful altitude data and diagrams. A drawback is that it is specialized to provide data for Ubiquiti devices only.","title":"Ubiquiti Line of Sight"},{"location":"infrastructure/software/#other-resources","text":"","title":"Other resources"},{"location":"infrastructure/software/#facebook-isp-toolbox-line-of-sight","text":"A web-based line of sight tool provided by Facebook Connectivity that utilizes public LiDAR data. Unfortunately LiDAR data for the Seattle area is not present yet, although there is data for some areas in Tacoma.","title":"Facebook ISP Toolbox Line of Sight"},{"location":"infrastructure/software/#facebook-isp-toolbox-market-evaluator","text":"A web-based market evaluator provided by Facebook Connectivity that can be used to provide more context about the areas around potential network sites. It offers information about other service providers in the area, average household income, median speeds, and current lowest broadband price available.","title":"Facebook ISP Toolbox Market Evaluator"},{"location":"learn/cable-crimping/","text":"Crimping Ethernet Cables In this article, you'll all about crimping ethernet cables! What is crimping an ethernet cable? Crimping an ethernet cable is the process of attaching connectors onto the ends of ethernet cables. This process is also called 'RJ45 crimping' because RJ45 is the name of the connectors that are used for ethernet cables, and they are what is being crimped. Why? Setting up networks involves setting up long ethernet cable connections between different devices. Instead of buying premade ethernet cables of varying lengths (e.g. 5ft, 10ft, 50ft, etc.), it's more practical to just have a big spool of cabling that we can roll out and cut to the exact length we need. Therefore we need to be able to attach RJ45 connectors to the ends of these cut cables so that we can actually plug them in! Crimping Kit Here are some tools you should have in your crimping kit! RJ45 Crimping Tool An RJ45 crimping tool is the most essential tool. Although it's technically possible to crimp ethernet cables without this specialized tool, it's not very practical for crimping lots of cables. Its primary utility is to do the actual 'crimping' part of compressing/crimping the tiny gold pins in the RJ45 connector onto the ethernet cables. It also has blades that can be used to cut or strip wires. Cable Stripper Cable strippers are used to take off the protecting shielding around cables and expose the inner wires. You can also do the same thing with a simple blade or pair of scissors. The trickiest part about stripping cables is trying to avoid cutting the inner wires! RJ45 Connectors RJ45 connectors are required for crimping because they feature the 8 golden pins that get crimped onto the 8 wires of the ethernet cable. They are what get plugged into ethernet ports! They also feature a latch/clip that locks the ethernet cable into the port once it is plugged in. RJ45 Boots RJ45 boots can be optionally used to protect the RJ45 connector. It provides insulation and prevents the cable from being breaking easily. They have to put slipped onto the cable before you put on the RJ45 connectors though! RJ45 Cable Tester RJ45 cable testers allow you to guarantee that you did the job correctly! They have two pieces that separate from each other, and you plug each end of your crimped ethernet cable into the port on each piece. Then you turn it on and the cable tester will test the connection for all 8 pins. If there are any missing lights on any of the pins, it means that you messed up somewhere and have to restart! How to Crimp an Ethernet Cable Assuming you have a crimping kit and an ethernet cable that needs to be crimped, here are all the steps! Step 0) Slip on the RJ45 boot (optional) Step 1) Strip the cable Push the cable into the razor slot of the strip tool and turn it around the cable to make an even cut around the sheath. Careful not to nick the wires inside! Unwrap the blue foil shielding and plastic to uncover the twisted wire pairs. Push the copper grounding wire to the side. (Ignore the white string.) Step 2) Organize the wires In this step, you'll be taking the 8 colored wires inside the ethernet cable and putting them into the correct ordering of colors. NOTE This is the hardest part of crimping! The wires are small and are hard to control. Take your time and make sure you do this step correctly! Otherwise you might have to go back and restart. Step 2.1) Untwist the wires There should be 4 pairs of wires: green, brown, orange, and blue. Each pair has a solid-colored wire and a striped-colored wire. Untwist these pairs and separate them into the 8 wires. Step 2.2) Straighten out wires After untwisting the wires, they are probably still kinked and look like they want to be twisted. In this step, you should carefully grab all the wires and try to straighten them out by pulling on them. This will prevent the wires from moving around later on. WARNING Don't break off the wires! Step 2.3) Lay out wires in order With your straightened out wires, put them into the correct order! Make sure that the wires are all flat and in line with each other. The ordering for these wires is: 1. Striped orange 2. Solid orange 3. Striped green 4. Solid blue 5. Striped blue 6. Solid green 7. Striped brown 8. Solid brown TIP After laying them out in order, straighten them out again as a group! This will help keep the wires together. Step 2.4) Trim the wires Trim the wires evenly to about 1/2 inch in length using scissors or the blade of your crimping tool. You want to make sure you have enough room for the wires to reach the end of the RJ45 connector. But also try to have room for the shielding of the cable to be inserted into the connector too. TIP You can put the wires side-by-side to the RJ45 connector to see how long you should cut it. Look at the next step to see what the final product looks like. TIP If you don't have the shielding inside of the connector, it makes it easier for the wires to snap off later, which is bad. TIP Make sure that you cut the wires evenly! Step 3) Slide wires into RJ45 connector Carefully slide your 8 wires into the connector. Make sure that the clip is facing away from you! If it is really hard to slide it into the connector, you probably didn't straighten out the wires enough in step 2.2 or 2.3. MORE INFO Inserting the wires with the clip facing away from you is the standard. However, you could technically do it in 'reverse' and insert the wires with the clip facing you, as long as you do it on both ends of the cable. You shouldn't do this in practice though because others would get confused when looking at your cable. Step 4) Crimp it Push the RJ45 connector into the slot of your crimping tool for RJ45 connectors. The slot should be labeled something like \"8P\" for the 8-pin RJ45 connector that you're using. In this step, you're doing the actual 'crimping' part and crimping/compressing/stabbing the 8 golden pins on the RJ45 connector into the 8 colored wires. TIP Squeeze as hard as you can! You need to make sure that all 8 pins are crimped. Step 5) Test it Slide the two pieces of the tester apart and plug each of the cable ends into either piece. Turn the switch to \u201cOn\u201d or \u201cSlow.\u201d If it's working, all 8 numbers should be flashing green. If any of them are not showing green, it means something is wrong and you have to redo it! The RJ45 connector can't be reused once it's crimped, so you should just cut the end off and start back at step 1. If everything is green, then you're done! If you had a cable boot, you can push the boots onto the RJ45 connector now. Resources Workshop Slides ISOC ICS Training Workshop Videos Crimping Tutorial (2 mins) Cable Testing Only need first 7 minutes for the basics Websites Color Coding Diagrams Crimping Comic From People's Open Network + sudomesh Shopping Crimping Kit ($23) Comes with a nice case Might need to buy your own batteries for cable tester Crimping Kit ($17) Might need to buy your own batteries for cable tester","title":"Crimping Ethernet Cables"},{"location":"learn/cable-crimping/#crimping-ethernet-cables","text":"In this article, you'll all about crimping ethernet cables!","title":"Crimping Ethernet Cables"},{"location":"learn/cable-crimping/#what-is-crimping-an-ethernet-cable","text":"Crimping an ethernet cable is the process of attaching connectors onto the ends of ethernet cables. This process is also called 'RJ45 crimping' because RJ45 is the name of the connectors that are used for ethernet cables, and they are what is being crimped.","title":"What is crimping an ethernet cable?"},{"location":"learn/cable-crimping/#why","text":"Setting up networks involves setting up long ethernet cable connections between different devices. Instead of buying premade ethernet cables of varying lengths (e.g. 5ft, 10ft, 50ft, etc.), it's more practical to just have a big spool of cabling that we can roll out and cut to the exact length we need. Therefore we need to be able to attach RJ45 connectors to the ends of these cut cables so that we can actually plug them in!","title":"Why?"},{"location":"learn/cable-crimping/#crimping-kit","text":"Here are some tools you should have in your crimping kit!","title":"Crimping Kit"},{"location":"learn/cable-crimping/#rj45-crimping-tool","text":"An RJ45 crimping tool is the most essential tool. Although it's technically possible to crimp ethernet cables without this specialized tool, it's not very practical for crimping lots of cables. Its primary utility is to do the actual 'crimping' part of compressing/crimping the tiny gold pins in the RJ45 connector onto the ethernet cables. It also has blades that can be used to cut or strip wires.","title":"RJ45 Crimping Tool"},{"location":"learn/cable-crimping/#cable-stripper","text":"Cable strippers are used to take off the protecting shielding around cables and expose the inner wires. You can also do the same thing with a simple blade or pair of scissors. The trickiest part about stripping cables is trying to avoid cutting the inner wires!","title":"Cable Stripper"},{"location":"learn/cable-crimping/#rj45-connectors","text":"RJ45 connectors are required for crimping because they feature the 8 golden pins that get crimped onto the 8 wires of the ethernet cable. They are what get plugged into ethernet ports! They also feature a latch/clip that locks the ethernet cable into the port once it is plugged in.","title":"RJ45 Connectors"},{"location":"learn/cable-crimping/#rj45-boots","text":"RJ45 boots can be optionally used to protect the RJ45 connector. It provides insulation and prevents the cable from being breaking easily. They have to put slipped onto the cable before you put on the RJ45 connectors though!","title":"RJ45 Boots"},{"location":"learn/cable-crimping/#rj45-cable-tester","text":"RJ45 cable testers allow you to guarantee that you did the job correctly! They have two pieces that separate from each other, and you plug each end of your crimped ethernet cable into the port on each piece. Then you turn it on and the cable tester will test the connection for all 8 pins. If there are any missing lights on any of the pins, it means that you messed up somewhere and have to restart!","title":"RJ45 Cable Tester"},{"location":"learn/cable-crimping/#how-to-crimp-an-ethernet-cable","text":"Assuming you have a crimping kit and an ethernet cable that needs to be crimped, here are all the steps!","title":"How to Crimp an Ethernet Cable"},{"location":"learn/cable-crimping/#step-0-slip-on-the-rj45-boot-optional","text":"","title":"Step 0) Slip on the RJ45 boot (optional)"},{"location":"learn/cable-crimping/#step-1-strip-the-cable","text":"Push the cable into the razor slot of the strip tool and turn it around the cable to make an even cut around the sheath. Careful not to nick the wires inside! Unwrap the blue foil shielding and plastic to uncover the twisted wire pairs. Push the copper grounding wire to the side. (Ignore the white string.)","title":"Step 1) Strip the cable"},{"location":"learn/cable-crimping/#step-2-organize-the-wires","text":"In this step, you'll be taking the 8 colored wires inside the ethernet cable and putting them into the correct ordering of colors. NOTE This is the hardest part of crimping! The wires are small and are hard to control. Take your time and make sure you do this step correctly! Otherwise you might have to go back and restart.","title":"Step 2) Organize the wires"},{"location":"learn/cable-crimping/#step-21-untwist-the-wires","text":"There should be 4 pairs of wires: green, brown, orange, and blue. Each pair has a solid-colored wire and a striped-colored wire. Untwist these pairs and separate them into the 8 wires.","title":"Step 2.1) Untwist the wires"},{"location":"learn/cable-crimping/#step-22-straighten-out-wires","text":"After untwisting the wires, they are probably still kinked and look like they want to be twisted. In this step, you should carefully grab all the wires and try to straighten them out by pulling on them. This will prevent the wires from moving around later on. WARNING Don't break off the wires!","title":"Step 2.2) Straighten out wires"},{"location":"learn/cable-crimping/#step-23-lay-out-wires-in-order","text":"With your straightened out wires, put them into the correct order! Make sure that the wires are all flat and in line with each other. The ordering for these wires is: 1. Striped orange 2. Solid orange 3. Striped green 4. Solid blue 5. Striped blue 6. Solid green 7. Striped brown 8. Solid brown TIP After laying them out in order, straighten them out again as a group! This will help keep the wires together.","title":"Step 2.3) Lay out wires in order"},{"location":"learn/cable-crimping/#step-24-trim-the-wires","text":"Trim the wires evenly to about 1/2 inch in length using scissors or the blade of your crimping tool. You want to make sure you have enough room for the wires to reach the end of the RJ45 connector. But also try to have room for the shielding of the cable to be inserted into the connector too. TIP You can put the wires side-by-side to the RJ45 connector to see how long you should cut it. Look at the next step to see what the final product looks like. TIP If you don't have the shielding inside of the connector, it makes it easier for the wires to snap off later, which is bad. TIP Make sure that you cut the wires evenly!","title":"Step 2.4) Trim the wires"},{"location":"learn/cable-crimping/#step-3-slide-wires-into-rj45-connector","text":"Carefully slide your 8 wires into the connector. Make sure that the clip is facing away from you! If it is really hard to slide it into the connector, you probably didn't straighten out the wires enough in step 2.2 or 2.3. MORE INFO Inserting the wires with the clip facing away from you is the standard. However, you could technically do it in 'reverse' and insert the wires with the clip facing you, as long as you do it on both ends of the cable. You shouldn't do this in practice though because others would get confused when looking at your cable.","title":"Step 3) Slide wires into RJ45 connector"},{"location":"learn/cable-crimping/#step-4-crimp-it","text":"Push the RJ45 connector into the slot of your crimping tool for RJ45 connectors. The slot should be labeled something like \"8P\" for the 8-pin RJ45 connector that you're using. In this step, you're doing the actual 'crimping' part and crimping/compressing/stabbing the 8 golden pins on the RJ45 connector into the 8 colored wires. TIP Squeeze as hard as you can! You need to make sure that all 8 pins are crimped.","title":"Step 4) Crimp it"},{"location":"learn/cable-crimping/#step-5-test-it","text":"Slide the two pieces of the tester apart and plug each of the cable ends into either piece. Turn the switch to \u201cOn\u201d or \u201cSlow.\u201d If it's working, all 8 numbers should be flashing green. If any of them are not showing green, it means something is wrong and you have to redo it! The RJ45 connector can't be reused once it's crimped, so you should just cut the end off and start back at step 1. If everything is green, then you're done! If you had a cable boot, you can push the boots onto the RJ45 connector now.","title":"Step 5) Test it"},{"location":"learn/cable-crimping/#resources","text":"","title":"Resources"},{"location":"learn/cable-crimping/#workshop-slides","text":"ISOC ICS Training Workshop","title":"Workshop Slides"},{"location":"learn/cable-crimping/#videos","text":"Crimping Tutorial (2 mins) Cable Testing Only need first 7 minutes for the basics","title":"Videos"},{"location":"learn/cable-crimping/#websites","text":"Color Coding Diagrams Crimping Comic From People's Open Network + sudomesh","title":"Websites"},{"location":"learn/cable-crimping/#shopping","text":"Crimping Kit ($23) Comes with a nice case Might need to buy your own batteries for cable tester Crimping Kit ($17) Might need to buy your own batteries for cable tester","title":"Shopping"},{"location":"learn/lte-networks/","text":"LTE Networks Our network uses a 4G LTE network architecture. Understanding everything is a huge challenge, but understanding it at a high-level is very achievable. Consider exploring all of these links as they all complement each other well! Helpful videos How Cell Service Actually Works Broad overview of cellular technologies, from the very basics up to advanced topics, in only 20 minutes AT&T Archives Video: AMPS Old video from 1978 about an older version of cell networks (AMPS) Very very high quality and gives good background on cellular networks Learn 4G LTE Network Architecture Quick high-level overview of 4G LTE architecture Only shows diagrams How does your mobile phone work? Very good visual for the entire system of how cell phones work today Has 3d animation so it's easier to understand conceptually Doesn't go over LTE architecture specifically Driving Factors of LTE Architecture Gain more understanding of the context around LTE architecture Helpful articles YaleBTS LTE Concepts Explains basically ALL the parts of the LTE architecture TutorialsPoint LTE Network Architecture Gives overview of LTE architecture Not very detailed, but it's a good introduction to the individual components of LTE Open5GS Introduction Documentation for Open5GS, the repo that our networks rely on This is the most applicable to our specific network because it's what we actually use.","title":"LTE Networks"},{"location":"learn/lte-networks/#lte-networks","text":"Our network uses a 4G LTE network architecture. Understanding everything is a huge challenge, but understanding it at a high-level is very achievable. Consider exploring all of these links as they all complement each other well!","title":"LTE Networks"},{"location":"learn/lte-networks/#helpful-videos","text":"How Cell Service Actually Works Broad overview of cellular technologies, from the very basics up to advanced topics, in only 20 minutes AT&T Archives Video: AMPS Old video from 1978 about an older version of cell networks (AMPS) Very very high quality and gives good background on cellular networks Learn 4G LTE Network Architecture Quick high-level overview of 4G LTE architecture Only shows diagrams How does your mobile phone work? Very good visual for the entire system of how cell phones work today Has 3d animation so it's easier to understand conceptually Doesn't go over LTE architecture specifically Driving Factors of LTE Architecture Gain more understanding of the context around LTE architecture","title":"Helpful videos"},{"location":"learn/lte-networks/#helpful-articles","text":"YaleBTS LTE Concepts Explains basically ALL the parts of the LTE architecture TutorialsPoint LTE Network Architecture Gives overview of LTE architecture Not very detailed, but it's a good introduction to the individual components of LTE Open5GS Introduction Documentation for Open5GS, the repo that our networks rely on This is the most applicable to our specific network because it's what we actually use.","title":"Helpful articles"},{"location":"learn/networking/","text":"Learn about Computer Networks This page is currently in development. For now, please view our lesson on computer networks from our Digital Stewards curriculum here . TODO","title":"Networking"},{"location":"learn/networking/#learn-about-computer-networks","text":"This page is currently in development. For now, please view our lesson on computer networks from our Digital Stewards curriculum here . TODO","title":"Learn about Computer Networks"},{"location":"learn/wireless-communication/","text":"Crash Course in Wireless Communication Authored by: Dominick Ta Last updated: June 18th, 2021 TODO: [ ] Expand on transmit/receiving in antennas section [ ] Finish communication section [ ] Finish protocols section This is a crash course in wireless communication: the magical way that we humans send information (e.g. music, messages, text, videos) to each other over long distances, without a wire. Wireless communication is primarily powered by radio waves, a physical thing that we as humans cannot see or touch. In this article you will gain a layman's understanding of how wireless communication works. Specifically, this article will cover the following broad topics: Radio waves and the physics behind it Antennas How data is communicated via radio waves Examples of wireless communication technologies This is not meant to be a comprehensive article, so there will be a lot of simplifications, analogies, and informal explanations. Please let me know at domta@cs.uw.edu if you see any inaccuracies, misconceptions, or misnomers. Radio waves Radio waves are just oscillations of energy that exist throughout our environment. They can be generated naturally by lightning, or artifically by equipment made by humans such as cell phones. The technical definition of radio waves are that they are a form of 'electromagnetic radiation'. Other types of electromagnetic radiation include visible light (what we get from the sun), infrared, and X-rays. How radio waves are created To understand why radio waves are considered a type of electromagnetic radiation, it is helpful to know how we generate radio waves. This section is not important to fully understand, but it is good to skim to have a basic idea of how this stuff works. We create radio waves by moving electrons back and forth within an electrically conductive object (an object made of material that allows electrons to move freely, such as metals). This works because electrons have special properties: All electrons have ' electric fields ' surrounding them that attracts and repels other charged particles. All moving electrons produce ' magnetic fields ' that are the basis for how magnets work. Therefore, when electrons move there are two fields (electric & magnetic) present that combine to create creates an ' electromagnetic field '. By moving an electron back and forth in an oscillating (repetitive) motion, these electromagnetic fields are constantly being disturbed/moved in a way that creates electromagnetic waves or equivalently, electromagnetic radiation . In this GIF below, we see the charge of an antenna changing, representing electrons moving back and forth within the antenna. This creates pulsating electromagnetic waves. Note: in this particular GIF, it is actually only showing the pulsating electric fields. In this GIF below, we are able to fully see an electromagnetic wave with a 3D representation. The red component of the wave represents the electric field, while the blue component represents the magnetic field. Notice how they are perpendicular to each other! This is also why the GIF above didn't show both fields: it was a 2D representation! In this section, we learned about radio waves. In essence, you can just think of radio waves as physical phenomenon that look like these sine waves: With the proper hardware and knowledge, we can create any type of wave we want! We can vary frequencies (how fast the waves go by), amplitudes (how tall/powerful the waves are), and phases (what position within the loop the wave is in). Frequency & Wavelength It turns out that a fundamental characteristic of any given radio wave is its frequency. Therefore, it's important to know what 'frequency' means, and how it relates to the concept of 'wavelength'. Frequency is a measurement of how often something happens. In the case of radio waves, it measures how many cycles of a radio wave occurs in a certain amonut of time. Frequency is measured in the units of hertz (Hz) , which represents cycles per second. If we had a radio wave that passed through 10 complete cycles in a minute, it would have a frequency of 0.16Hz (10 cycles per 60 seconds). Wavelength measures the physical length of a single cycle in a radio wave. If we could see radio waves and measure it, and we saw that there was a distance of 10-feet between two of the peaks in a radio wave, then that radio wave would have a wavelength of 10 feet. In the image above, we can see a natural relationship between frequency and wavelength: a faster frequency means that you will have shorter wavelengths! Or conversely, longer wavelengths means that there is a slower frequency! When one value goes up, the other value has to go down; this is called an inverse relationship . So if I told you that I had a radio wave with a very high frequency, you could figure out that my radio wave has very small wavelengths. And actually, if I told you exactly what frequency my radio waves travelled at, you'd be able to figure out the exact size of the wavelength! For example, if I told you I had a radio wave with a frequency of 10Hz, you would be able to figure out that my radio wave has a wavelength of approximately 30,000,000 meters. This is because radio waves are a physical thing, so they always travel through air at the same speed: the speed of light. This means we can consistently convert back and forth between frequency and wavelength with some basic algebra. The basic equation, where c is the speed of light (3.0 x 10^8 m/s), is: frequency (Hz) * wavelength (m) = c So given a radio wave with a frequency of 10Hz, to solve for wavelength, I just need to take the speed of light and divide by 10! 30,000,000 divided by 10 gives a value of a wavelength of 30,000,000 meters. To summarize this section, frequency and wavelength are important properties of radio waves. Although they describe different aspects of a radio wave, they are essentially synonymous because we can easily convert between the two. When people are talking about radio waves, you may hear them talk in terms of frequencies (e.g. megahertz, gigahertz) or you may hear them talk in terms of wavelengths (e.g. meters). In the next section, we'll see why exactly frequency is such an important characteristic of radio waves. How we distinguish between radio waves An important thing to remember about radio waves is that nowadays they surround us everywhere we go! Radio waves are powerful because they can go through obstacles like walls, and they can potentially propagate over huge distances at a relatively cheap cost. Radio waves are used to communicate information in technologies such as GPS, WiFi, Bluetooth, cell phones, music radio stations, and more. The problem with this is that these radio waves interfere with each other and combine together to become a jumbled mess! They are all co-existing within the same space, and they can't avoid each other. In real life we don't get to have nice, simple, isolated radio wave like in the previous sections, we get a mix of radio waves coming all at once! And somehow, we have to find and narrow down the signal we care about. Imagine you are at an airport and you're trying to talk to your friend, but it is super crowded and theres people talking and shouting all around you. It would be super hard to hear your friend and have a conversation! (In this situation the people represent devices that use radio waves, the sound waves represent radio waves, and the voices & words represent the data we want to transmit) But if your friend talks loud enough, even though your ear is full of noise from everyone else in the airport, you can still figure out what your friend is trying to tell you. This is because you're smart enough to recognize what your friend's voice sounds like and you can focus on that voice. We can do the same thing with radio waves of different frequencies! (Being able to focus on your friend's specific voice is analogous to being able to focus on only radio waves of a specific frequency) In the image below, we can see a red radio wave. This radio wave is actually the combination of 5 radio waves of different frequencies! In the blue, we see this same signal analyzed and split into the individual components, located at its respective frequencies. (Analogy: 5 people talking at the same time, what your ears hear is the red. Your brain recognizing that these are 5 different voices is the blue) The mathematical process of going from the red representation (the signal in the time domain) to the blue representation (the signal in the frequency domain) is called the \"Discrete Fourier Transform\". You may also hear about something called the \"Fast Fourier Transform\" which is the discrete fourier transform, but a very clever and fast way to calculate it. (Intuitively, the DFT/FFT is essentially comparing a bunch of sine waves of varying frequencies to the signal detected, and calculating how similar they are. This would be like your brain iterating through all possible human voices and checking to see how strongly your ears hear that particular voice) This means that even though there may be a jumbled mess of radio waves surrounding us at all times, if they are radio waves of different frequencies, we are able to distinguish between all these different frequencies using some old fashioned engineering. This non-trivial fact is what allows our world to have so many different types of devices communicating wirelessly all at the same time! They are all using radio waves, all in the same space, but at different frequencies! This is why radio waves are identified by their frequency (or wavelength). Summary In this section on radio waves, we learned about what they are, the basics of how they're produced, frequency & wavelength, and how we distinguish between different radio waves co-existing in the same space. These are the fundamental concepts behind the physics of radio waves that engineers take advantage of. In the next section on antennas, we will learn what they are and how they are used to efficiently propagate/send radio waves. In another section, we will learn more about how exactly we manipulate radio waves to convey the information we want to send. Antennas We use antennas in our everyday lives, but most people don't know how they work. We use antennas on cars, on buildings, and even within our computers and phones! How they work Material properties Antennas are all made up of electrically conductive material. This usually means antennas are made out of metals like copper. Materials that are electrically conductive allow electrons to freely move throughout it. The opposite type of material would be electrically insulating material. As a real-life analogy, imagine a typical swimming pool filled with water. A normal pool like this allows people to freely swim through it! In this analogy the water represents electrically conductive material and people represent electrons. Now imagine a piece of copper metal as being that pool of water. Because copper is electrically conductive, electrons can freely move through it! An analogy for an electrically insulating material would be a swimming pool filled with jello/pudding/gelatin. If someone tried diving into that pool, they wouldn't get very far and it'd be super hard or impossible to swim through it. In this analogy the jello/pudding/gelatin represents electrically insulating material and people represent electrons. Now imagine a piece of plastic being that pool of water. Because plastic is electrically insulating, eletrons are mostly stuck where they are inside the plastic! Taking advantage of moving electrons Antennas need to be electrically conductive because they transmit and receive radio waves by taking advantage of the movement of electrons. In the above section on \"How radio waves are created\" we learned that radio waves are generated by moving an electron back and forth in an oscillating (repetitive) motion. This oscillation of electrons occurs in the antennas. If antennas weren't electrically conductive, these electrons wouldn't be able to move and create these radio waves! How transmitting with an antenna works In order to transmit with an antenna, the antenna needs to be connected to an electrical component that can control the movement of these electrons. TODO: expand/clarify? How receiving with an antenna works Antennas receive radio signals passively. As the electromagnetic fields are manipulated in the environment around an antenna, the electrons in the antenna move accordingly (because of how the physics of it work). By monitoring the movement of these electrons, the respective radio wave can be captured. TODO: expand/clarify? Communicating with radio waves How do we as humans harness this power of manipulating radio waves as communication? We agree on a bunch of different rules and conventions on how we will manipulating these radio waves to convey information efficiently and responsibly. In the United States, most of these rules are established and enforced by the Federal Communications Commission (FCC). An analogy To lay a conceptual foundation for this relatively abstract section, here is an analogy. Suppose we have two friends with the simple names of \"A\" and \"B\" that want to talk to each other over a distance, but they cant see, hear, or touch each other over that distance. The only way method of communication they have is a really long piece of rope between them. So once they travel far away from each other, they will each be holding one end of the rope and will be trying to communicate. But before they travel far away from each other, they need to talk to each other to figure out a set of rules of communication so that they can understand each other when they feel the rope moving. When coming up with these rules for communication, one of the first things that A & B need to do is come up with a language. Because A & B really like computers, they chose the language of computers: binary. They chose this language because it is super simple and effective; it only has two letters (1 and 0) and you can send tons of information by being clever with 1s and 0s (thats what computers do). Now the next thing that A & B needs to do is figure out how to use the rope to send these 1s and 0s. Here are some ideas they brainstormed together: * If the rope is moving up and down (oscillating), then that is considered a 1. If the rope is not moving, then that is considered a 0. * If the rope is oscillating super fast then that's a 1, but if the rope is oscillating slowly then that's a 0. * If the rope is oscillating super fast then that's a 0, but if the rope is oscillating slowly then that's a 1. * If the rope is oscillating with a big height then that's a 1, but if the rope is oscillating with small height then that's a 0. A & B realized two properties of the movement of the rope that they could capture: (1) frequency, how fast the rope is oscillating, and (2) amplitude, how tall the rope is when its oscillating. The term for manipulating these properties is \"modulation\". This makes sense because a dictionary definition of modulation is \"to adjust\"; modulation is just a fancy word for changing. In this analogy, the material/medium of communication was the rope. But in our context, the material/medium of communication is radio waves. Both of these materials have frequency and amplitude that can be modulated to send information, and demodulated to receive that information. In the next section, we'll learn about frequency and amplitude modulation. We'll also learn about some other types of modulation that don't have a direct real-life connection to ropes. Modulation Frequency modulation Amplitude modulation Other modulation schemes Frequency allocation Baseband signal and carrier signals Bandwidth Bands and Channels Wireless Communication Protocols In the previous section on \"Communicating with Radio Waves\", we learned how we are able to transmit messages wirelessly over radio waves via modulation. We understood this through our analogy with friends A & B who came up with a way to send letters in their binary language (1 and 0) through a rope. Assuming they were able to do this successfully, they could send long streams of messages that look something like \"101011100101010101010\". But that's still not enough! They need to come up with a 'protocol' for deciphering what this message actually means! For example, they could come up with a simple protocol for sending messages about how their day went. * The first 3 letters would represent how their day went: \"101\" could mean that they had a good day, and \"010\" could mean that they had a bad day, and maybe \"110\" means that their day went okay. * The next 3 letters would represent the weather: \"100\" could mean that it was sunny and \"101\" could mean that it was rainy. We have these same types of protocols in real life that are much more complicated. They are carefully designed to ensure security (can people intercept messages?), reliability (what if the message gets messed up in certain places?), and efficiency (how much useful data can we send at a time?). WiFi Bluetooth Cellular communication","title":"Wireless Communication"},{"location":"learn/wireless-communication/#crash-course-in-wireless-communication","text":"Authored by: Dominick Ta Last updated: June 18th, 2021 TODO: [ ] Expand on transmit/receiving in antennas section [ ] Finish communication section [ ] Finish protocols section This is a crash course in wireless communication: the magical way that we humans send information (e.g. music, messages, text, videos) to each other over long distances, without a wire. Wireless communication is primarily powered by radio waves, a physical thing that we as humans cannot see or touch. In this article you will gain a layman's understanding of how wireless communication works. Specifically, this article will cover the following broad topics: Radio waves and the physics behind it Antennas How data is communicated via radio waves Examples of wireless communication technologies This is not meant to be a comprehensive article, so there will be a lot of simplifications, analogies, and informal explanations. Please let me know at domta@cs.uw.edu if you see any inaccuracies, misconceptions, or misnomers.","title":"Crash Course in Wireless Communication"},{"location":"learn/wireless-communication/#radio-waves","text":"Radio waves are just oscillations of energy that exist throughout our environment. They can be generated naturally by lightning, or artifically by equipment made by humans such as cell phones. The technical definition of radio waves are that they are a form of 'electromagnetic radiation'. Other types of electromagnetic radiation include visible light (what we get from the sun), infrared, and X-rays.","title":"Radio waves"},{"location":"learn/wireless-communication/#how-radio-waves-are-created","text":"To understand why radio waves are considered a type of electromagnetic radiation, it is helpful to know how we generate radio waves. This section is not important to fully understand, but it is good to skim to have a basic idea of how this stuff works. We create radio waves by moving electrons back and forth within an electrically conductive object (an object made of material that allows electrons to move freely, such as metals). This works because electrons have special properties: All electrons have ' electric fields ' surrounding them that attracts and repels other charged particles. All moving electrons produce ' magnetic fields ' that are the basis for how magnets work. Therefore, when electrons move there are two fields (electric & magnetic) present that combine to create creates an ' electromagnetic field '. By moving an electron back and forth in an oscillating (repetitive) motion, these electromagnetic fields are constantly being disturbed/moved in a way that creates electromagnetic waves or equivalently, electromagnetic radiation . In this GIF below, we see the charge of an antenna changing, representing electrons moving back and forth within the antenna. This creates pulsating electromagnetic waves. Note: in this particular GIF, it is actually only showing the pulsating electric fields. In this GIF below, we are able to fully see an electromagnetic wave with a 3D representation. The red component of the wave represents the electric field, while the blue component represents the magnetic field. Notice how they are perpendicular to each other! This is also why the GIF above didn't show both fields: it was a 2D representation! In this section, we learned about radio waves. In essence, you can just think of radio waves as physical phenomenon that look like these sine waves: With the proper hardware and knowledge, we can create any type of wave we want! We can vary frequencies (how fast the waves go by), amplitudes (how tall/powerful the waves are), and phases (what position within the loop the wave is in).","title":"How radio waves are created"},{"location":"learn/wireless-communication/#frequency-wavelength","text":"It turns out that a fundamental characteristic of any given radio wave is its frequency. Therefore, it's important to know what 'frequency' means, and how it relates to the concept of 'wavelength'. Frequency is a measurement of how often something happens. In the case of radio waves, it measures how many cycles of a radio wave occurs in a certain amonut of time. Frequency is measured in the units of hertz (Hz) , which represents cycles per second. If we had a radio wave that passed through 10 complete cycles in a minute, it would have a frequency of 0.16Hz (10 cycles per 60 seconds). Wavelength measures the physical length of a single cycle in a radio wave. If we could see radio waves and measure it, and we saw that there was a distance of 10-feet between two of the peaks in a radio wave, then that radio wave would have a wavelength of 10 feet. In the image above, we can see a natural relationship between frequency and wavelength: a faster frequency means that you will have shorter wavelengths! Or conversely, longer wavelengths means that there is a slower frequency! When one value goes up, the other value has to go down; this is called an inverse relationship . So if I told you that I had a radio wave with a very high frequency, you could figure out that my radio wave has very small wavelengths. And actually, if I told you exactly what frequency my radio waves travelled at, you'd be able to figure out the exact size of the wavelength! For example, if I told you I had a radio wave with a frequency of 10Hz, you would be able to figure out that my radio wave has a wavelength of approximately 30,000,000 meters. This is because radio waves are a physical thing, so they always travel through air at the same speed: the speed of light. This means we can consistently convert back and forth between frequency and wavelength with some basic algebra. The basic equation, where c is the speed of light (3.0 x 10^8 m/s), is: frequency (Hz) * wavelength (m) = c So given a radio wave with a frequency of 10Hz, to solve for wavelength, I just need to take the speed of light and divide by 10! 30,000,000 divided by 10 gives a value of a wavelength of 30,000,000 meters. To summarize this section, frequency and wavelength are important properties of radio waves. Although they describe different aspects of a radio wave, they are essentially synonymous because we can easily convert between the two. When people are talking about radio waves, you may hear them talk in terms of frequencies (e.g. megahertz, gigahertz) or you may hear them talk in terms of wavelengths (e.g. meters). In the next section, we'll see why exactly frequency is such an important characteristic of radio waves.","title":"Frequency & Wavelength"},{"location":"learn/wireless-communication/#how-we-distinguish-between-radio-waves","text":"An important thing to remember about radio waves is that nowadays they surround us everywhere we go! Radio waves are powerful because they can go through obstacles like walls, and they can potentially propagate over huge distances at a relatively cheap cost. Radio waves are used to communicate information in technologies such as GPS, WiFi, Bluetooth, cell phones, music radio stations, and more. The problem with this is that these radio waves interfere with each other and combine together to become a jumbled mess! They are all co-existing within the same space, and they can't avoid each other. In real life we don't get to have nice, simple, isolated radio wave like in the previous sections, we get a mix of radio waves coming all at once! And somehow, we have to find and narrow down the signal we care about. Imagine you are at an airport and you're trying to talk to your friend, but it is super crowded and theres people talking and shouting all around you. It would be super hard to hear your friend and have a conversation! (In this situation the people represent devices that use radio waves, the sound waves represent radio waves, and the voices & words represent the data we want to transmit) But if your friend talks loud enough, even though your ear is full of noise from everyone else in the airport, you can still figure out what your friend is trying to tell you. This is because you're smart enough to recognize what your friend's voice sounds like and you can focus on that voice. We can do the same thing with radio waves of different frequencies! (Being able to focus on your friend's specific voice is analogous to being able to focus on only radio waves of a specific frequency) In the image below, we can see a red radio wave. This radio wave is actually the combination of 5 radio waves of different frequencies! In the blue, we see this same signal analyzed and split into the individual components, located at its respective frequencies. (Analogy: 5 people talking at the same time, what your ears hear is the red. Your brain recognizing that these are 5 different voices is the blue) The mathematical process of going from the red representation (the signal in the time domain) to the blue representation (the signal in the frequency domain) is called the \"Discrete Fourier Transform\". You may also hear about something called the \"Fast Fourier Transform\" which is the discrete fourier transform, but a very clever and fast way to calculate it. (Intuitively, the DFT/FFT is essentially comparing a bunch of sine waves of varying frequencies to the signal detected, and calculating how similar they are. This would be like your brain iterating through all possible human voices and checking to see how strongly your ears hear that particular voice) This means that even though there may be a jumbled mess of radio waves surrounding us at all times, if they are radio waves of different frequencies, we are able to distinguish between all these different frequencies using some old fashioned engineering. This non-trivial fact is what allows our world to have so many different types of devices communicating wirelessly all at the same time! They are all using radio waves, all in the same space, but at different frequencies! This is why radio waves are identified by their frequency (or wavelength).","title":"How we distinguish between radio waves"},{"location":"learn/wireless-communication/#summary","text":"In this section on radio waves, we learned about what they are, the basics of how they're produced, frequency & wavelength, and how we distinguish between different radio waves co-existing in the same space. These are the fundamental concepts behind the physics of radio waves that engineers take advantage of. In the next section on antennas, we will learn what they are and how they are used to efficiently propagate/send radio waves. In another section, we will learn more about how exactly we manipulate radio waves to convey the information we want to send.","title":"Summary"},{"location":"learn/wireless-communication/#antennas","text":"We use antennas in our everyday lives, but most people don't know how they work. We use antennas on cars, on buildings, and even within our computers and phones!","title":"Antennas"},{"location":"learn/wireless-communication/#how-they-work","text":"","title":"How they work"},{"location":"learn/wireless-communication/#material-properties","text":"Antennas are all made up of electrically conductive material. This usually means antennas are made out of metals like copper. Materials that are electrically conductive allow electrons to freely move throughout it. The opposite type of material would be electrically insulating material. As a real-life analogy, imagine a typical swimming pool filled with water. A normal pool like this allows people to freely swim through it! In this analogy the water represents electrically conductive material and people represent electrons. Now imagine a piece of copper metal as being that pool of water. Because copper is electrically conductive, electrons can freely move through it! An analogy for an electrically insulating material would be a swimming pool filled with jello/pudding/gelatin. If someone tried diving into that pool, they wouldn't get very far and it'd be super hard or impossible to swim through it. In this analogy the jello/pudding/gelatin represents electrically insulating material and people represent electrons. Now imagine a piece of plastic being that pool of water. Because plastic is electrically insulating, eletrons are mostly stuck where they are inside the plastic!","title":"Material properties"},{"location":"learn/wireless-communication/#taking-advantage-of-moving-electrons","text":"Antennas need to be electrically conductive because they transmit and receive radio waves by taking advantage of the movement of electrons. In the above section on \"How radio waves are created\" we learned that radio waves are generated by moving an electron back and forth in an oscillating (repetitive) motion. This oscillation of electrons occurs in the antennas. If antennas weren't electrically conductive, these electrons wouldn't be able to move and create these radio waves!","title":"Taking advantage of moving electrons"},{"location":"learn/wireless-communication/#how-transmitting-with-an-antenna-works","text":"In order to transmit with an antenna, the antenna needs to be connected to an electrical component that can control the movement of these electrons. TODO: expand/clarify?","title":"How transmitting with an antenna works"},{"location":"learn/wireless-communication/#how-receiving-with-an-antenna-works","text":"Antennas receive radio signals passively. As the electromagnetic fields are manipulated in the environment around an antenna, the electrons in the antenna move accordingly (because of how the physics of it work). By monitoring the movement of these electrons, the respective radio wave can be captured. TODO: expand/clarify?","title":"How receiving with an antenna works"},{"location":"learn/wireless-communication/#communicating-with-radio-waves","text":"How do we as humans harness this power of manipulating radio waves as communication? We agree on a bunch of different rules and conventions on how we will manipulating these radio waves to convey information efficiently and responsibly. In the United States, most of these rules are established and enforced by the Federal Communications Commission (FCC).","title":"Communicating with radio waves"},{"location":"learn/wireless-communication/#an-analogy","text":"To lay a conceptual foundation for this relatively abstract section, here is an analogy. Suppose we have two friends with the simple names of \"A\" and \"B\" that want to talk to each other over a distance, but they cant see, hear, or touch each other over that distance. The only way method of communication they have is a really long piece of rope between them. So once they travel far away from each other, they will each be holding one end of the rope and will be trying to communicate. But before they travel far away from each other, they need to talk to each other to figure out a set of rules of communication so that they can understand each other when they feel the rope moving. When coming up with these rules for communication, one of the first things that A & B need to do is come up with a language. Because A & B really like computers, they chose the language of computers: binary. They chose this language because it is super simple and effective; it only has two letters (1 and 0) and you can send tons of information by being clever with 1s and 0s (thats what computers do). Now the next thing that A & B needs to do is figure out how to use the rope to send these 1s and 0s. Here are some ideas they brainstormed together: * If the rope is moving up and down (oscillating), then that is considered a 1. If the rope is not moving, then that is considered a 0. * If the rope is oscillating super fast then that's a 1, but if the rope is oscillating slowly then that's a 0. * If the rope is oscillating super fast then that's a 0, but if the rope is oscillating slowly then that's a 1. * If the rope is oscillating with a big height then that's a 1, but if the rope is oscillating with small height then that's a 0. A & B realized two properties of the movement of the rope that they could capture: (1) frequency, how fast the rope is oscillating, and (2) amplitude, how tall the rope is when its oscillating. The term for manipulating these properties is \"modulation\". This makes sense because a dictionary definition of modulation is \"to adjust\"; modulation is just a fancy word for changing. In this analogy, the material/medium of communication was the rope. But in our context, the material/medium of communication is radio waves. Both of these materials have frequency and amplitude that can be modulated to send information, and demodulated to receive that information. In the next section, we'll learn about frequency and amplitude modulation. We'll also learn about some other types of modulation that don't have a direct real-life connection to ropes.","title":"An analogy"},{"location":"learn/wireless-communication/#modulation","text":"","title":"Modulation"},{"location":"learn/wireless-communication/#frequency-modulation","text":"","title":"Frequency modulation"},{"location":"learn/wireless-communication/#amplitude-modulation","text":"","title":"Amplitude modulation"},{"location":"learn/wireless-communication/#other-modulation-schemes","text":"","title":"Other modulation schemes"},{"location":"learn/wireless-communication/#frequency-allocation","text":"","title":"Frequency allocation"},{"location":"learn/wireless-communication/#baseband-signal-and-carrier-signals","text":"","title":"Baseband signal and carrier signals"},{"location":"learn/wireless-communication/#bandwidth","text":"","title":"Bandwidth"},{"location":"learn/wireless-communication/#bands-and-channels","text":"","title":"Bands and Channels"},{"location":"learn/wireless-communication/#wireless-communication-protocols","text":"In the previous section on \"Communicating with Radio Waves\", we learned how we are able to transmit messages wirelessly over radio waves via modulation. We understood this through our analogy with friends A & B who came up with a way to send letters in their binary language (1 and 0) through a rope. Assuming they were able to do this successfully, they could send long streams of messages that look something like \"101011100101010101010\". But that's still not enough! They need to come up with a 'protocol' for deciphering what this message actually means! For example, they could come up with a simple protocol for sending messages about how their day went. * The first 3 letters would represent how their day went: \"101\" could mean that they had a good day, and \"010\" could mean that they had a bad day, and maybe \"110\" means that their day went okay. * The next 3 letters would represent the weather: \"100\" could mean that it was sunny and \"101\" could mean that it was rainy. We have these same types of protocols in real life that are much more complicated. They are carefully designed to ensure security (can people intercept messages?), reliability (what if the message gets messed up in certain places?), and efficiency (how much useful data can we send at a time?).","title":"Wireless Communication Protocols"},{"location":"learn/wireless-communication/#wifi","text":"","title":"WiFi"},{"location":"learn/wireless-communication/#bluetooth","text":"","title":"Bluetooth"},{"location":"learn/wireless-communication/#cellular-communication","text":"","title":"Cellular communication"}]} \ No newline at end of file diff --git a/search/worker.js b/search/worker.js new file mode 100644 index 0000000..8628dbc --- /dev/null +++ b/search/worker.js @@ -0,0 +1,133 @@ +var base_path = 'function' === typeof importScripts ? '.' : '/search/'; +var allowSearch = false; +var index; +var documents = {}; +var lang = ['en']; +var data; + +function getScript(script, callback) { + console.log('Loading script: ' + script); + $.getScript(base_path + script).done(function () { + callback(); + }).fail(function (jqxhr, settings, exception) { + console.log('Error: ' + exception); + }); +} + +function getScriptsInOrder(scripts, callback) { + if (scripts.length === 0) { + callback(); + return; + } + getScript(scripts[0], function() { + getScriptsInOrder(scripts.slice(1), callback); + }); +} + +function loadScripts(urls, callback) { + if( 'function' === typeof importScripts ) { + importScripts.apply(null, urls); + callback(); + } else { + getScriptsInOrder(urls, callback); + } +} + +function onJSONLoaded () { + data = JSON.parse(this.responseText); + var scriptsToLoad = ['lunr.js']; + if (data.config && data.config.lang && data.config.lang.length) { + lang = data.config.lang; + } + if (lang.length > 1 || lang[0] !== "en") { + scriptsToLoad.push('lunr.stemmer.support.js'); + if (lang.length > 1) { + scriptsToLoad.push('lunr.multi.js'); + } + if (lang.includes("ja") || lang.includes("jp")) { + scriptsToLoad.push('tinyseg.js'); + } + for (var i=0; i < lang.length; i++) { + if (lang[i] != 'en') { + scriptsToLoad.push(['lunr', lang[i], 'js'].join('.')); + } + } + } + loadScripts(scriptsToLoad, onScriptsLoaded); +} + +function onScriptsLoaded () { + console.log('All search scripts loaded, building Lunr index...'); + if (data.config && data.config.separator && data.config.separator.length) { + lunr.tokenizer.separator = new RegExp(data.config.separator); + } + + if (data.index) { + index = lunr.Index.load(data.index); + data.docs.forEach(function (doc) { + documents[doc.location] = doc; + }); + console.log('Lunr pre-built index loaded, search ready'); + } else { + index = lunr(function () { + if (lang.length === 1 && lang[0] !== "en" && lunr[lang[0]]) { + this.use(lunr[lang[0]]); + } else if (lang.length > 1) { + this.use(lunr.multiLanguage.apply(null, lang)); // spread operator not supported in all browsers: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator#Browser_compatibility + } + this.field('title'); + this.field('text'); + this.ref('location'); + + for (var i=0; i < data.docs.length; i++) { + var doc = data.docs[i]; + this.add(doc); + documents[doc.location] = doc; + } + }); + console.log('Lunr index built, search ready'); + } + allowSearch = true; + postMessage({config: data.config}); + postMessage({allowSearch: allowSearch}); +} + +function init () { + var oReq = new XMLHttpRequest(); + oReq.addEventListener("load", onJSONLoaded); + var index_path = base_path + '/search_index.json'; + if( 'function' === typeof importScripts ){ + index_path = 'search_index.json'; + } + oReq.open("GET", index_path); + oReq.send(); +} + +function search (query) { + if (!allowSearch) { + console.error('Assets for search still loading'); + return; + } + + var resultDocuments = []; + var results = index.search(query); + for (var i=0; i < results.length; i++){ + var result = results[i]; + doc = documents[result.ref]; + doc.summary = doc.text.substring(0, 200); + resultDocuments.push(doc); + } + return resultDocuments; +} + +if( 'function' === typeof importScripts ) { + onmessage = function (e) { + if (e.data.init) { + init(); + } else if (e.data.query) { + postMessage({ results: search(e.data.query) }); + } else { + console.error("Worker - Unrecognized message: " + e); + } + }; +} diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..0f8724e --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 0000000..c6c4131 Binary files /dev/null and b/sitemap.xml.gz differ