Compare commits

...
611 Commits
Author SHA1 Message Date
Meysam Parvizi 7977b7a62b docs(contributing): describe the versioning and release policy 2026-10-10 01:38:06 +02:00
Meysam Parvizi 81f7e6f7d8 ci: release a patch version automatically when the map changes
Every master build fingerprints the rendered map (PNG without the revision line) and compares it with the latest release. A changed map publishes the next patch release with the PDF and PNG; otherwise the site keeps showing the latest version. Minor and major releases are started manually from the workflow. Versions restart at v2.0.0.
2026-10-10 01:38:06 +02:00
Meysam Parvizi 44bd12bc7e chore: ignore local render output in dist/ 2026-10-10 01:15:05 +02:00
Meysam Parvizi cc34538ad2 feat(site): add PDF and PNG download buttons for the rendered roadmap
Shown only when the CI-rendered files exist next to the site; on phones they replace the Print button.
2026-10-10 01:15:05 +02:00
Meysam Parvizi c9bc31d3fc docs(readme): explain how the roadmap image is generated
Point contributors to README.md and site/map.json as the sources of the
roadmap and drop the remaining Visio references.
2026-10-10 01:15:05 +02:00
Meysam Parvizi 7f1c6e37fe chore: remove the Visio source file
The roadmap is now drawn by the explorer from README.md and
site/map.json, so the .vsdx file and its lock-file ignores are no
longer needed.
2026-10-10 01:15:05 +02:00
Meysam Parvizi 263527e9f7 ci: build, render and deploy the roadmap explorer
Run the tests, assemble the site with build revision info, render the
PDF and PNG, deploy to GitHub Pages from master and attach the PDF and
PNG to published releases.
2026-10-10 01:15:05 +02:00
Meysam Parvizi 0b60717b39 feat(site): render the roadmap to PDF and PNG from the site
site/tools/render.mjs serves the site, opens it in headless Chromium
and writes an A4 PDF and a 2x PNG of the map using the print styles.
2026-10-10 01:15:05 +02:00
Meysam Parvizi fdb9eaba1c feat(site): add interactive roadmap explorer
Static site in site/ (vanilla ES modules, no build step) that reads
README.md at runtime and draws the roadmap map.

- Map view: Software, Software ∩ Hardware, Hardware and Soft skills
  areas, importance colors, area/importance filters, connections
  between topics, pan/zoom, A4 print stylesheet
- Outline view, search and a details panel with descriptions and
  learning resources
- site/map.json is the editable source of the map (bands, clusters,
  importance); layout.js places it on an A4 page with a minimum spacing
- Credits with creator logo, CC BY-SA badge and build revision
- Sofia Sans Semi Condensed bundled under the OFL
- Tests for the README parser, map mapping and layout
2026-10-10 01:15:05 +02:00
Meysam Parvizi 234f89bb38 docs(readme): move OS-level topics out of Embedded Linux 2026-10-09 18:16:16 +02:00
Meysam Parvizi adca1e955b chore: add .gitignore for editor, Visio and tooling files 2026-10-09 18:13:28 +02:00
Meysam Parvizi f436b82539 chore(github): simplify issue forms and PR template 2026-10-09 18:02:24 +02:00
Meysam Parvizi 52aa4a7efa chore(github): add issue forms for resources, link problems and roadmap changes 2026-10-09 17:56:22 +02:00
Meysam Parvizi 8ba3a7f235 ci(link-check): open an issue even when lychee fails 2026-10-09 17:29:08 +02:00
Meysam 0d29726169 Merge pull request #87 from m3y54m/dependabot/github_actions/DavidAnson/markdownlint-cli2-action-24
Bump DavidAnson/markdownlint-cli2-action from 19 to 24
2026-10-09 17:21:11 +02:00
Meysam 770423adea Merge pull request #86 from m3y54m/dependabot/github_actions/actions/checkout-7
Bump actions/checkout from 5 to 7
2026-10-09 17:20:57 +02:00
Meysam 420a3632fe Merge pull request #85 from m3y54m/dependabot/github_actions/peter-evans/create-issue-from-file-6
Bump peter-evans/create-issue-from-file from 5 to 6
2026-10-09 17:20:43 +02:00
dependabot[bot] 03fba9cb09 Bump DavidAnson/markdownlint-cli2-action from 19 to 24
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 19 to 24.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v19...v24)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: '24'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-09 15:15:14 +00:00
dependabot[bot] c1aa63c1aa Bump actions/checkout from 5 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-09 15:15:12 +00:00
Meysam Parvizi 811bf6f779 ci(link-check): tune lychee for bot-blocking hosts and bump checkout
Accept 3xx/403/429, exclude hosts that fail HTTP/2 or 404 for bots, repository badge links and a broken cert chain; raise timeout to 60s; use actions/checkout@v5.
2026-10-09 17:13:09 +02:00
dependabot[bot] fb6d9c892b Bump peter-evans/create-issue-from-file from 5 to 6
Bumps [peter-evans/create-issue-from-file](https://github.com/peter-evans/create-issue-from-file) from 5 to 6.
- [Release notes](https://github.com/peter-evans/create-issue-from-file/releases)
- [Commits](https://github.com/peter-evans/create-issue-from-file/compare/v5...v6)

---
updated-dependencies:
- dependency-name: peter-evans/create-issue-from-file
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-09 15:10:16 +00:00
Meysam Parvizi a6e614bd7b ci: add link checker, README convention check and PR templat 2026-10-09 17:08:26 +02:00
Meysam Parvizi 718dc4c1a2 docs(readme): align content and formatting with contributing guidelines 2026-10-09 16:26:50 +02:00
Meysam Parvizi 95798c6c33 docs(readme): add replacement links and fix section placement 2026-10-09 16:19:39 +02:00
Meysam Parvizi 8d0f027bce docs(readme): fix broken, moved and mislabeled links
- Update redirected/renamed URLs (ThreadX, Mbed OS, LittleFS, ESP-IDF,
  FreeRTOS, CUDA, DuckDuckGo AI, edX, etc.)
- Point EEFS to github.com/nasa/EEFS (old NASA host no longer resolves)
- Correct labels and markers (Arduino Core, MIPI I3C, FPGA video)
- Remove dead, hijacked or outdated links
- Add replacement resources for removed links (KiCad, Beej's Guide to C,
  Linux Kernel Labs, meta-raspberrypi, MathWorks HIL, Scrum Guide, etc.)
2026-10-09 15:31:53 +02:00
Meysam 79949a4123 Merge pull request #84 from Anwitha-bm/patch-1
grammar check
2026-10-09 13:02:01 +02:00
Anwitha Koneru b37dd7ab0c grammer check 2026-10-06 12:53:36 +05:30
Meysam 0738fcbd8f Merge pull request #83 from 3omargaad/patch-2
Added Embedded Systems Arm Course Resource
2026-10-04 02:39:49 +02:00
Omar Gaad efa2b58a71 Added Embedded Systems Arm Course Resource 2026-10-03 20:28:18 +01:00
Meysam a9f4eac5c9 Merge pull request #82 from dchithinh/add-qemu-embedded-linux-guide
docs: add ARM embedded Linux QEMU guide
2026-07-23 12:55:55 +03:30
Thinh Do fbc2bbe074 docs: add ARM embedded Linux QEMU guide 2026-07-20 15:54:31 +07:00
Meysam ede541e217 Merge pull request #80 from 3omargaad/patch-1
Update README.md
2026-07-12 17:29:19 +03:30
Omar Gaad 3f84cb4946 Update README.md
Spelling Correction: changed "Assoication" to Association
2026-07-12 15:38:44 +03:00
Meysam 0bc41bf6ac Merge pull request #79 from colonelblacc/add-interactive-skill-tree
docs: add interactive embedded systems skill tree link
2026-07-11 02:20:47 +03:30
colonelblac 37334a10d8 docs: add interactive embedded systems skill tree link 2026-07-11 03:18:37 +05:30
Meysam c43065728c Update README.md 2026-06-20 17:41:03 +03:30
Meysam 399e9072d8 Merge pull request #76 from AntonioBerna/feature/add-protocols
add embedded protocols 🚀
2026-05-10 21:47:40 +02:00
AntonioBerna b43ee3b3c9 add embedded protocols 2026-05-10 16:58:43 +02:00
Meysam 9b0589de9a Merge pull request #74 from AntonioBerna/chore/add-coursera-stm32-deep-dive
docs: add Coursera ARM Cortex STM32 Deep Dive course
2026-04-25 22:02:07 +02:00
AntonioBerna 6d2434c093 docs: add Coursera ARM Cortex STM32 Deep Dive course 2026-04-25 14:04:58 +02:00
Meysam ce19924102 Merge pull request #73 from AntonioBerna/add-new-ref-links
Add curated links across relevant roadmap sections
2026-04-12 14:26:12 +02:00
AntonioBerna e42e802909 Add curated links across relevant roadmap sections 2026-04-11 16:34:26 +02:00
Meysam c6fb798231 Merge pull request #71 from AntonioBerna/chore/add-learning-resources-links
docs: add suggested learning resources links
2026-03-16 19:56:35 +01:00
AntonioBerna 4d72f70ac1 added new link 2026-03-16 17:31:56 +01:00
AntonioBerna 287aba4c37 docs: add suggested learning resources links 2026-03-16 02:28:48 +01:00
Meysam 363e2182dc Merge pull request #70 from AntonioBerna/docs/add-bare-metal-cpp
docs: add Bare Metal C++ guide to C++ section
2026-03-13 03:33:18 +01:00
Meysam a51558de95 Merge pull request #69 from AntonioBerna/docs/add-osek-resources
docs: add OSEK OS resources to AUTOSAR section
2026-03-13 03:32:47 +01:00
AntonioBerna 3243a38153 docs: add Bare Metal C++ guide to C++ section
- Add 'Bare Metal C++' online book by Alexey Rybachuk
2026-03-10 13:47:18 +01:00
AntonioBerna 8e983933b6 docs: add OSEK OS resources to AUTOSAR section
Closes #65

- Add OSEK OS Overview article from autosartoday.com
- Add OSEK/VDX OS Specification 2.2.3 (official PDF)
- Add OSEK/VDX OIL Specification 2.4.1 (official PDF)
2026-03-10 13:45:33 +01:00
Meysam d1265280e0 Merge pull request #67 from AntonioBerna/docs/add-embedded-rust-esp32-italian-course
docs: add Italian embedded Rust ESP32 course
2026-02-23 00:16:33 +01:00
Meysam 1698faccbf Merge pull request #66 from AntonioBerna/docs/add-sccb-and-awesome-electronics
docs: add SCCB spec and awesome-electronics
2026-02-23 00:16:00 +01:00
AntonioBerna 10a1359485 docs: add Italian embedded Rust ESP32 course 2026-02-13 18:31:22 +01:00
AntonioBerna c341c849cd docs: add SCCB spec and awesome-electronics 2026-02-13 18:14:47 +01:00
Meysam Parvizi 4f4058ca04 Merge pull request #62 from Javatti/master
Add 2 Courses of Udemy website for PCB Design
2025-12-15 22:21:42 +03:30
mohammad javad 982dc5e439 Merge branch 'm3y54m:master' into master 2025-12-02 14:32:54 +03:30
javad 407857803c add 2 course source
+ Add 2 cources with titles : "Advanced Hardware and PCB Design Masterclass 2022" and  "High-Speed Board Design Course System On Module" in Udemy  for PCB Design
2025-12-02 14:30:37 +03:30
Meysam Parvizi bfbf6715da Update README.md 2025-11-24 01:07:29 +03:30
Meysam Parvizi 655c4393f5 Update README.md 2025-11-24 01:02:10 +03:30
Meysam Parvizi 3ec8d86998 Merge pull request #61 from Javatti/master
Update README.md
2025-11-12 23:30:42 +03:30
javad 09b6b687cc Update README.md
Change a wrong address link YouTube for Ethernet Course
2025-11-12 10:19:35 +03:30
Meysam Parvizi 8cdcebd74f Update README.md 2025-10-06 20:24:29 +03:30
Meysam Parvizi 4db7a2607d Update README.md 2025-10-06 20:19:58 +03:30
Meysam Parvizi 0cbb009e31 Merge pull request #58 from VivanPanda/master
Add ArduinoMap resource to roadmap list
2025-09-26 10:55:51 +03:30
VivanPanda f9ef67a2f2 Add ArduinoMap resource to README 2025-09-26 07:50:38 +04:00
Meysam Parvizi 40cd50535d Update README.md 2025-09-18 20:33:55 +02:00
Meysam Parvizi c36274440b Improve static analysis workflow for Markdown linting 2025-09-05 10:03:06 +02:00
Meysam Parvizi dd63c2f1f0 Merge pull request #56 from AntonioBerna/feature/fix-code-of-conduct-error
Fix code of conduct workflow error 🚀
2025-09-05 10:50:05 +03:30
Meysam Parvizi d8573c8602 Update CONTRIBUTING.md 2025-09-05 10:38:32 +03:30
Antonio Bernardini e733e52cc0 fix code of conduct workflow error 🚀 2025-09-03 15:13:45 +02:00
Meysam Parvizi 2b3afb9077 Update CONTRIBUTING.md 2025-08-31 21:15:18 +03:30
Meysam Parvizi 4d43e82a8e Fix Markdown lint check errors 2025-08-31 17:11:46 +02:00
Meysam Parvizi 92344eff24 Update README.md 2025-08-31 18:10:47 +03:30
Meysam Parvizi 80a11187b7 Update README.md 2025-08-31 18:08:57 +03:30
Meysam Parvizi a9689ce9cc Update CONTRIBUTING.md 2025-08-31 17:54:57 +03:30
Meysam Parvizi 08fdaa683f Add CODE_OF_CONDUCT.md 2025-08-31 16:47:12 +03:30
Meysam Parvizi 0c11d5077f Add CONTRIBUTING.md with contributor guidelines 2025-08-31 16:47:05 +03:30
Meysam Parvizi d2fba71fdb Merge pull request #49 from Electrostat-Lab/cpp-resources
README.md: added resources by Stroustrup (C++ Creator)
2025-08-31 15:51:49 +03:30
Pavly Gerges (pavl_g) 4866358427 Update README.md 2025-07-27 13:57:09 -05:00
Pavly Gerges (pavl_g) ef2003e1c2 README.md: C++ publications section 2025-07-27 06:21:26 -05:00
Meysam Parvizi f6036f707d Merge pull request #55 from lzzzam/add_career_section
Update README.md
2025-07-25 22:12:42 +03:30
Meysam Parvizi 7a9a4349f0 Merge pull request #54 from barnabywalters/patch-1
Linked to USB in a nutshell
2025-07-25 22:11:06 +03:30
zambutol 6396c68a9a Update README.md
Add Career development section and link to best job board to land an embedded job
2025-07-23 13:34:41 +02:00
Barnaby Walters 7886954208 Linked to USB in a nutshell 2025-07-22 17:40:36 +02:00
Meysam Parvizi ccebf9e07e Merge pull request #53 from AntonioBerna/feature/fix-markdownlist-workflow
Fix markdownlist workflow 🎉
2025-05-15 22:48:02 +02:00
Antonio Bernardini bb2bdbf59a Fix markdownlist workflow 🎉 2025-05-10 00:57:20 +02:00
Meysam Parvizi 65ae1f8a4c Merge pull request #50 from AntonioBerna/feature/hw-simulation
Added new resources about circuit simulations 🚀
2025-05-10 00:18:18 +02:00
Meysam Parvizi 06e65e9636 Merge pull request #51 from liweisu59/master
Update README.md
2025-05-10 00:10:43 +02:00
Meysam Parvizi 79436daa42 Merge pull request #52 from liweisu59/patch-1
Update README.md
2025-05-10 00:09:42 +02:00
Antonio Bernardini f9bda303ab added new Android/IOS simulation app ⚡ 2025-04-30 23:33:04 +02:00
Liwei Su 7acecc07ea Update README.md
Add books about learning assembly related to arm
2025-04-30 11:57:51 +08:00
Liwei Su 3948044dbf Update README.md
Add book Practical UML Statecharts in C/C++ by Miro Samek for "State Machine" Section
2025-04-29 14:57:23 +08:00
Antonio Bernardini ce429616af Added new resources about circuit simulations 🚀 2025-04-28 07:33:09 +02:00
Pavly Gerges (pavl_g) bb299c97b8 README.md: typo correction 2025-04-14 06:49:31 -05:00
Pavly Gerges (pavl_g) 4f57e7322a README.md: added resources by Stroustrup (C++ Creator) 2025-04-14 06:44:59 -05:00
Meysam Parvizi 887acbf64d Update README.md 2025-04-14 00:18:08 +03:30
Meysam Parvizi dc2738a8cd Update README.md 2025-04-14 00:15:17 +03:30
Meysam Parvizi db7f1edbf8 Update README.md 2025-04-14 00:13:29 +03:30
Meysam Parvizi 73a7e40067 Merge pull request #48 from Electrostat-Lab/discrete-maths
[New Section] Discrete Mathematics for Embedded Engineers
2025-04-12 08:58:12 +02:00
Pavly Gerges (pavl_g) 9529ffefe4 README.md: typos and optimizations 2025-04-12 01:44:39 -05:00
Pavly Gerges (pavl_g) f2bd5403f5 README.md: advanced topics section 2025-04-12 01:39:48 -05:00
Pavly Gerges (pavl_g) faf2d121be Update README.md 2025-04-11 17:50:23 -05:00
Pavly Gerges (pavl_g) 17022df5e7 README.md: extracted some resources to basic maths section and moved advanced topics to the appendix 2025-04-11 17:48:11 -05:00
Pavly Gerges (pavl_g) 1bf34b4646 README.md: clearly defined goals of learning discrete mathematics 2025-04-11 10:34:22 -05:00
Pavly Gerges (pavl_g) 1ccad2d532 README.md: re-enforced listing by introducing the subject bullet-in 🔵 2025-04-11 10:32:45 -05:00
Pavly Gerges (pavl_g) da209028b1 README.md: added Guide to Discrete Mathematics by Gerard O'Regan 2025-04-11 10:28:41 -05:00
Pavly Gerges (pavl_g) 9dd36557a5 README.md: text correction and resources redirects 2025-04-10 18:00:26 -05:00
Pavly Gerges (pavl_g) e4afd0f6e9 README.md: introduced section for discrete mathematics 2025-04-10 16:15:53 -05:00
Meysam Parvizi 468a3e48e9 Update README.md 2025-04-07 08:54:24 +02:00
Meysam Parvizi bb8188ed79 Merge pull request #46 from lendzeg/master
Added 2 resources for C programming and Zephyr
2025-04-04 15:45:08 +02:00
Meysam Parvizi 144e1b0fa8 Merge pull request #45 from AntonioBerna/master
Added a new resource for learning ESP-IDF using tutorials 🚀
2025-04-04 15:44:44 +02:00
Nelson Rodriguez 260fe82159 Added:
- Coursera - C Programming with Linux Specialization 
- Introduction to Zephyr - Digikey
2025-04-03 11:11:57 -05:00
AntonioBerna 73f33549d5 Added a new resource for learning ESP-IDF using tutorials 🚀 2025-04-02 02:55:05 +02:00
Meysam Parvizi a1d90150e2 Merge pull request #44 from abakosh/update_link
Update  Inline Assembly Language  resource in README.md
2025-02-17 09:00:58 +01:00
abakosh cb9502e460 Update Inline Assembly Language resource in README.md 2025-02-07 16:31:04 +01:00
Meysam Parvizi 7c4b025264 Update README.md 2025-01-15 22:00:47 +01:00
Meysam Parvizi cf6060c9f5 Update README.md 2025-01-05 14:07:48 +01:00
Meysam Parvizi a71d798b6f Update README.md 2024-12-27 13:27:47 +01:00
Meysam Parvizi 0876545bfa Update README.md
Add embeddedrelated.com links
2024-12-26 20:09:20 +01:00
Meysam Parvizi bf47fa9812 Update README.md
Adding resource - Making Embedded Systems: Design Patterns for Great Software - Audiobook (#41)
2024-12-26 19:22:56 +01:00
Meysam Parvizi fcc0e6f4da Merge pull request #43 from AntonioBerna/feature/learn-c
Closes #42 ⚡
2024-12-26 19:11:17 +01:00
AntonioBerna 675d7ba4a0 added Learn C Programming and OOP with Dr. Chuck resource, closes #42 2024-12-16 15:48:35 +01:00
Meysam Parvizi 55f5f21e0a Update README.md 2024-11-28 22:31:04 +01:00
Meysam Parvizi 8a5db76a73 Update README.md 2024-11-28 22:28:16 +01:00
Meysam Parvizi 1e4123b58a Merge pull request #40 from AntonioBerna/master
Added something resources 🎉
2024-11-28 21:11:19 +01:00
AntonioBerna 74f9bc837b added yt playlist 🚀 2024-11-28 00:27:43 +01:00
AntonioBerna d4e968e341 added a new Rusy resource 🦀 2024-11-28 00:18:00 +01:00
Meysam Parvizi b9648c1a9e Update README.md 2024-11-14 08:24:57 +01:00
Meysam Parvizi 6b4b93fb95 Merge pull request #38 from ScottGibb/add-static-analysis
Added Markdownlint and static analysis
2024-11-12 20:11:22 +01:00
Scott Gibb 39baabe911 replaced workflow 2024-11-11 15:59:51 +00:00
Scott Gibb f2c0b113a1 Correcting workflow error 2024-11-11 15:58:48 +00:00
Scott Gibb 83d70447a6 Added Markdownlint and static analysis 2024-11-11 15:56:50 +00:00
Meysam Parvizi fb30c35787 Update README.md 2024-11-10 15:49:22 +01:00
Meysam Parvizi 1f7274bd3b Update README.md 2024-11-10 02:17:17 +01:00
Meysam Parvizi 16cace6831 Merge pull request #35 from AntonioBerna/master
added some utilities 🚀
2024-11-09 19:41:12 +01:00
AntonioBerna 88556f05dd added some utilities 🚀 2024-11-09 15:31:26 +01:00
Meysam Parvizi 2adfc55547 Update README.md 2024-10-31 09:01:40 +01:00
Meysam Parvizi 21a28a5628 Merge pull request #34 from lyrakisk/patch-1
Add "The Elements of Computing Systems" to computer architecture
2024-10-31 08:41:48 +01:00
Konstantinos Lyrakis 04a6ddc56f Update README.md 2024-10-30 22:45:48 +01:00
Meysam Parvizi 04666a1778 Update README.md 2024-10-30 10:30:36 +01:00
Meysam Parvizi bd4d1fabd1 Update README.md for GTest resources 2024-10-30 10:27:44 +01:00
Meysam Parvizi 522ac5cd6b Added Modern C book (#32) 2024-10-22 22:47:27 +02:00
Meysam Parvizi 4b23d44b5b Merge pull request #29 from AntonioBerna/master
Update 🔗 RTOS Fundamentals link 🔥
2024-09-12 03:34:48 +02:00
AntonioBerna 6b1b5e37a3 Update 🔗 RTOS Fundamentals link 🔥 2024-09-12 01:30:39 +02:00
Meysam Parvizi b64be6582a Update FUNDING.yml 2024-09-03 03:17:52 +02:00
Meysam Parvizi 13e2b8f0e3 Merge pull request #26 from rrxmzl/patch-1
Paul McWhorter update new series of videos for arduino
2024-08-11 14:55:46 +03:30
rrxmzl d534a7981a Paul McWhorter update new series of videos for arduino 2024-08-11 14:55:01 +08:00
Meysam Parvizi 302ad1cf7b Merge pull request #25 from ScottGibb/patch-1
Adding Bootlin For Yocto/Linux Development
2024-08-02 11:55:54 +03:30
Scott Gibb 57ea594a3a Update README.md
Add Bootlin Link
2024-08-02 09:21:39 +01:00
Meysam Parvizi a046909c32 Merge pull request #24 from francisrstokes/francisrstokes-new-youtube-channel
Added new youtubechannel to the courses and youtube section
2024-07-31 00:05:21 +03:30
Francis Stokes 4b90facad3 Added new youtubechannel to the courses and youtube section 2024-07-30 15:26:14 +02:00
Meysam Parvizi 853cad223d Update FUNDING.yml 2024-07-23 08:39:06 +03:30
Meysam Parvizi 0bc4ce88bb Merge pull request #23 from AntonioBerna/master
added new resource about Algorithms & Data Structures 🔥
2024-07-15 10:50:12 +03:30
AntonioBerna 4a9566d917 added new resource about Algorithms & Data Structures 🔥 2024-07-15 07:18:43 +02:00
Meysam Parvizi 71fdc4b068 Merge pull request #22 from AntonioBerna/master
Added rustlings link - small exercises to get you used to reading and writing Rust code 🔥
2024-07-12 11:16:44 +03:30
AntonioBerna 8fa251fddf Added rustlings link - small exercises to get you used to reading and writing Rust code 🔥 2024-07-11 22:16:16 +02:00
Meysam Parvizi 4bb7b1908f Merge pull request #21 from AntonioBerna/master
Added new unit testing tool for embedded 🎉
2024-07-01 11:21:00 +03:30
AntonioBerna 63d3632f9d Added new unit testing tool for embedded 🎉 2024-07-01 05:16:15 +02:00
Meysam Parvizi b68f6e5902 Update README.md 2024-06-24 13:14:03 +03:30
Meysam Parvizi fb4d604bdc Merge pull request #20 from AntonioBerna/master
Added new link contains projects about Embedded Engineering 🔥
2024-06-23 14:21:57 +03:30
AntonioBerna 02b7d51af0 Added new link contains projects about Embedded Engineering 🔥 2024-06-23 02:52:32 +02:00
Meysam Parvizi f919ec4963 Merge pull request #19 from AntonioBerna/master
added new tool for improve PCBs design using AI 🎉
2024-06-07 13:16:40 +03:30
AntonioBerna 42be5a3cff added new tool for improve PCBs design using AI 🎉 2024-06-06 13:04:45 +02:00
Meysam Parvizi 7e812ea832 Update README.md (#18)
Added a link to "Operating System development tutorials in Rust on the Raspberry Pi"
2024-06-04 17:31:17 +03:30
Meysam Parvizi 8eb6b979d8 Update README.md 2024-05-14 16:15:19 +03:30
Meysam Parvizi 374bcc818e Update README.md 2024-04-02 22:12:08 +03:30
Meysam Parvizi 420d375700 Update README.md 2024-03-13 20:05:33 +03:30
Meysam Parvizi 70d9a7da14 Update README.md 2024-03-12 11:56:00 +03:30
Meysam Parvizi 2254f0845c Update README.md 2024-03-12 10:22:27 +03:30
Meysam Parvizi 086066b83a Update README.md 2024-03-07 11:47:43 +03:30
Meysam Parvizi 52c087ccbd Update README.md 2024-03-02 15:38:30 +03:30
Meysam Parvizi 42aea2bcf1 Update README.md 2024-02-27 01:39:29 +03:30
Meysam Parvizi 5f2112a51f Update README.md 2024-02-23 16:32:43 +03:30
Meysam Parvizi c0c61ee0ee Update FUNDING.yml 2024-02-21 17:03:47 +03:30
Meysam Parvizi 74d1409f73 Update FUNDING.yml 2024-02-21 17:02:50 +03:30
Meysam Parvizi 670eaaa6c2 Update README.md 2024-02-21 01:07:52 +03:30
Meysam Parvizi 18772b293c Update README.md 2024-02-08 20:28:18 +03:30
Meysam Parvizi 269bcb7595 Update README.md 2024-02-05 15:51:52 +03:30
Meysam Parvizi 79911f668b Update README.md 2024-01-31 18:59:03 +03:30
Meysam Parvizi 7bb76db5fa Update README.md 2024-01-31 18:56:54 +03:30
Meysam Parvizi 33b34ab8c2 Update README.md 2024-01-20 22:16:10 +03:30
Meysam Parvizi 4fe59cd3a1 Update README.md 2023-12-30 15:25:12 +03:30
Meysam Parvizi 7415c0d4f3 Update README.md 2023-12-30 15:14:23 +03:30
Meysam Parvizi c29d43b8c5 Update README.md 2023-12-30 14:25:09 +03:30
Meysam Parvizi 36bead8d9c Update README.md 2023-12-29 17:00:27 +03:30
Meysam Parvizi 13d85a2f45 Update README.md 2023-12-24 18:01:15 +03:30
Meysam Parvizi 4895841f30 Update README.md 2023-12-23 18:13:51 +03:30
Meysam Parvizi f64f741c49 Update README.md 2023-12-23 17:56:03 +03:30
Meysam Parvizi e8fbc9cf79 Update README.md 2023-12-23 17:53:33 +03:30
Meysam Parvizi 05d948f6de Update README.md 2023-12-23 15:52:50 +03:30
Meysam Parvizi 57f2758660 Minor changes 2023-12-23 15:49:47 +03:30
Meysam Parvizi 5f47d664bc Minor changes 2023-12-23 15:42:57 +03:30
Meysam Parvizi 5d9c3c8028 Update README.md 2023-12-23 02:06:55 +03:30
Meysam Parvizi ee01886513 Update README.md 2023-12-22 02:59:36 +03:30
Meysam Parvizi 353baa47f4 Update README.md 2023-12-21 21:14:44 +03:30
Meysam Parvizi b3b0ecae1b Update README.md 2023-12-21 18:55:32 +03:30
Meysam Parvizi 0a5f341b17 Update README.md 2023-12-21 18:46:50 +03:30
Meysam Parvizi 1786cf3931 Update README.md 2023-12-21 13:01:20 +03:30
Meysam Parvizi 82e28f1446 Update README.md 2023-12-21 12:19:07 +03:30
Meysam Parvizi a9782873c3 Update README.md 2023-12-21 00:23:27 +03:30
Meysam Parvizi 3dd6b97969 Update README.md 2023-12-21 00:21:59 +03:30
Meysam Parvizi 86169624df Update README.md
Improve usage of emojis
2023-12-21 00:14:25 +03:30
Meysam Parvizi 2960e57a56 Update README.md 2023-12-19 20:13:26 +03:30
Meysam Parvizi 87bd904f30 Update README.md 2023-12-19 19:43:20 +03:30
Meysam Parvizi c5cae3c044 Update README.md 2023-12-19 15:12:17 +03:30
Meysam Parvizi 236431f72a Update README.md 2023-12-18 22:17:01 +03:30
Meysam Parvizi ee5dc44999 Add IoT 2023-12-18 22:04:16 +03:30
Meysam Parvizi cc0bf5622c Update README.md 2023-12-18 22:01:32 +03:30
Meysam Parvizi 551a74eada Update README.md 2023-12-18 22:00:32 +03:30
Meysam Parvizi 30c45e1ba2 Merge pull request #12 from DNedic/master
Add The most thoroughly commented embedded CMakeLists file
2023-12-18 20:50:03 +03:30
Djordje Nedic 9c29a0fe3f Add The most thoroughly commented embedded CMakeLists file 2023-12-18 17:40:00 +01:00
Meysam Parvizi 5ec4aa7ec3 Update README.md 2023-12-18 18:37:53 +03:30
Meysam Parvizi 62e7292549 Update README.md
Added some resources from Microchip University
2023-12-18 18:22:03 +03:30
Meysam Parvizi 1886ea26cb Update README.md 2023-12-18 17:21:21 +03:30
Meysam Parvizi 42b57bd190 Update README.md 2023-12-18 15:16:16 +03:30
Meysam Parvizi abe0259b8f Update README.md
Added introductory description to some topics
2023-12-18 13:46:39 +03:30
Meysam Parvizi 9d5d90418c Update README.md 2023-12-18 02:29:59 +03:30
Meysam Parvizi 7c2fb36823 Update README.md 2023-12-18 00:24:48 +03:30
Meysam Parvizi 81533b6ba5 Update README.md
Use emojis for good!
2023-12-18 00:14:19 +03:30
Meysam Parvizi b7299704cf Update README.md 2023-12-17 23:36:55 +03:30
Meysam Parvizi 0b77cbc325 Update README.md 2023-12-17 23:36:14 +03:30
Meysam Parvizi fac1fa2473 Update README.md 2023-12-17 23:13:38 +03:30
Meysam Parvizi 5676f51854 Update README.md 2023-12-17 23:11:30 +03:30
Meysam Parvizi 0a11df94a6 Update README.md
Added repository badges
2023-12-17 22:32:24 +03:30
Meysam Parvizi f693335b60 Update README.md 2023-12-17 21:00:21 +03:30
Meysam Parvizi 2c8bd526b0 Update README.md 2023-12-17 13:06:27 +03:30
Meysam Parvizi 8fe566cf4a Update README.md 2023-12-17 12:38:46 +03:30
Meysam Parvizi ee68906275 Update README.md 2023-12-17 12:28:43 +03:30
Meysam Parvizi 6dd04686e5 Minor changes 2023-12-17 12:23:13 +03:30
Meysam Parvizi 28d1ed568b Update README.md 2023-12-17 01:12:18 +03:30
Meysam Parvizi de59d32539 Update v1.2.0 2023-12-17 00:34:57 +03:30
Meysam Parvizi 41aaad321c Reorganize README.md for v1.2.0 2023-12-16 23:48:57 +03:30
Meysam Parvizi a9a9fe2b3b Added File Systems 2023-12-16 17:26:48 +03:30
Meysam Parvizi 555a712bde Update README.md 2023-12-16 01:22:23 +03:30
Meysam Parvizi bd93464d91 Update README.md 2023-12-15 14:15:45 +03:30
Meysam Parvizi 134f8bdca7 Update README.md 2023-12-12 00:12:18 +03:30
Meysam Parvizi 5fbbb7ca96 Update README.md 2023-12-11 23:59:47 +03:30
Meysam Parvizi 024624d2e5 Update README.md 2023-12-11 23:57:56 +03:30
Meysam Parvizi 365dbadc7e Update FUNDING.yml 2023-12-11 16:40:39 +03:30
Meysam Parvizi 3d34739037 Create FUNDING.yml 2023-12-11 16:35:53 +03:30
Meysam Parvizi fe82ac1193 Update README.md 2023-12-11 15:01:44 +03:30
Meysam Parvizi 6543b32216 Added AUTOSAR 2023-12-10 19:03:30 +03:30
Meysam Parvizi 15285e0be4 Update README.md 2023-12-10 18:53:43 +03:30
Meysam Parvizi 4ce9c26fb1 Update README.md 2023-12-09 22:16:51 +03:30
Meysam Parvizi 1bd6094cbe Update README.md 2023-12-09 11:02:43 +03:30
Meysam Parvizi 5ca196ca8c Update README.md 2023-12-08 21:28:51 +03:30
Meysam Parvizi 8ccca8e12b Update README.md 2023-12-07 20:11:36 +03:30
Meysam Parvizi 3cd1bcd62d Update README.md 2023-12-07 11:20:26 +03:30
Meysam Parvizi 25276210a1 Update README.md 2023-12-06 14:20:50 +03:30
Meysam Parvizi 00089ee209 Update README.md 2023-12-06 12:07:32 +03:30
Meysam Parvizi c3481aa490 Update README.md 2023-12-06 12:05:23 +03:30
Meysam Parvizi 3e905d0cf4 Update README.md 2023-12-06 11:51:07 +03:30
Meysam Parvizi 5c5174d573 Update README.md 2023-12-06 11:40:12 +03:30
Meysam Parvizi 07a45ef82e Update README.md 2023-12-06 11:16:16 +03:30
Meysam Parvizi d04ed29536 Update README.md 2023-12-06 01:08:14 +03:30
Meysam Parvizi 127a0d090e Update README.md 2023-12-06 00:26:56 +03:30
Meysam Parvizi 8284b9dced Update README.md 2023-12-05 23:58:54 +03:30
Meysam Parvizi 0f0ad015b1 Update README.md 2023-12-04 12:18:21 +03:30
Meysam Parvizi 91547a59d7 Update README.md 2023-12-04 12:12:50 +03:30
Meysam Parvizi 40acf885c9 Update README.md 2023-12-03 23:45:54 +03:30
Meysam Parvizi 800ea9fdf7 Update README.md 2023-12-03 22:28:41 +03:30
Meysam Parvizi 1f5e15bb92 Update README.md 2023-12-03 17:19:51 +03:30
Meysam Parvizi 2f7d00da83 Update README.md 2023-12-03 17:12:08 +03:30
Meysam Parvizi d3d989456f Update README.md 2023-12-02 10:42:54 +03:30
Meysam Parvizi 2c77977f38 Update README.md 2023-12-01 16:34:15 +03:30
Meysam Parvizi 62207b9762 Update README.md 2023-12-01 12:52:21 +03:30
Meysam Parvizi 092bcbf49c Update README.md 2023-12-01 12:42:32 +03:30
Meysam Parvizi 3ac8f30ea5 Update README.md 2023-12-01 12:33:34 +03:30
Meysam Parvizi d11e0d6098 Updated README.md and improved the order of contents 2023-12-01 12:15:03 +03:30
Meysam Parvizi e493fe33fe Update README.md 2023-12-01 11:44:53 +03:30
Meysam Parvizi 0de7e5b0ff Make fonts slightly larger 2023-12-01 11:28:24 +03:30
Meysam Parvizi 8fa54b081f Update README.md 2023-12-01 11:21:30 +03:30
Meysam Parvizi 6337d25195 Chages some titles and fonts 2023-11-30 19:28:02 +03:30
Meysam Parvizi 109142bbde Update README.md 2023-11-30 19:19:17 +03:30
Meysam Parvizi 3b5e739c9e Update README.md 2023-11-30 09:57:04 +03:30
Meysam Parvizi 970e80b134 Update README.md 2023-11-23 01:24:42 +03:30
Meysam Parvizi c9d150d5c7 Update README.md 2023-11-18 08:54:01 +03:30
Meysam Parvizi 391d17a873 Update README.md 2023-11-18 08:53:04 +03:30
Meysam Parvizi 791844b915 Update README.md 2023-11-13 12:53:29 +03:30
Meysam Parvizi 9eb05d193c Update README.md 2023-11-13 11:25:06 +03:30
Meysam Parvizi a9f1d70ebd Update README.md
Add PCM
2023-11-07 12:10:49 +03:30
Meysam Parvizi 7b44cf62ee Update README.md 2023-11-06 22:11:44 +03:30
Meysam Parvizi 91625afacd Update README.md 2023-11-06 22:07:58 +03:30
Meysam Parvizi 7154ce9b80 Update README.md 2023-11-06 18:30:54 +03:30
Meysam Parvizi 0c98d21c63 Update README.md 2023-10-30 19:33:22 +03:30
Meysam Parvizi ea904d1f7b Update README.md 2023-10-24 23:29:31 +03:30
Meysam Parvizi dad999d384 Update README.md 2023-10-22 00:28:10 +03:30
Meysam Parvizi 6b9429aace Update README.md 2023-10-16 13:20:42 +03:30
Meysam Parvizi ffe92d3faf Update README.md 2023-10-16 12:42:32 +03:30
Meysam Parvizi d4bb415e6c Update README.md 2023-10-16 12:29:16 +03:30
Meysam Parvizi 411ba4efc2 Update README.md 2023-10-15 17:22:00 +03:30
Meysam Parvizi 6daa7c32ed Update README.md 2023-10-15 15:42:55 +03:30
Meysam Parvizi aa8202438e Update README.md 2023-10-15 14:34:32 +03:30
Meysam Parvizi 4a208301db Update README.md 2023-10-14 21:47:52 +03:30
Meysam Parvizi e28fbb3258 Update README.md 2023-10-14 19:31:41 +03:30
Meysam Parvizi 5f2594d4fe Update README.md 2023-10-14 18:50:18 +03:30
Meysam Parvizi 26ec970d62 Update README.md 2023-10-13 15:46:39 +03:30
Meysam Parvizi 0b712030d8 Update README.md 2023-10-12 23:30:25 +03:30
Meysam Parvizi 1845ce150f Update README.md 2023-10-12 22:24:45 +03:30
Meysam Parvizi 8e5dac7e59 Update README.md 2023-10-12 10:49:48 +03:30
Meysam Parvizi ee17c54ac8 Update README.md 2023-10-12 09:40:38 +03:30
Meysam Parvizi 52dae1534d Update README.md 2023-10-11 21:49:33 +03:30
Meysam Parvizi cc9ad5d267 Update README.md 2023-10-11 19:09:52 +03:30
Meysam Parvizi c55ae0b2ed Update README.md 2023-10-11 19:00:08 +03:30
Meysam Parvizi d23c504eb4 Update README.md 2023-10-10 15:00:02 +03:30
Meysam Parvizi 106de1316e Update README.md 2023-10-10 14:56:31 +03:30
Meysam Parvizi b10a3e8ac8 Update README.md 2023-10-10 14:48:43 +03:30
Meysam Parvizi 236bc95b0a Update README.md 2023-10-10 14:47:27 +03:30
Meysam Parvizi d9cd7a23b5 Update README.md 2023-10-10 14:45:35 +03:30
Meysam Parvizi 8efa40a2e5 Update README.md 2023-10-10 11:35:43 +03:30
Meysam Parvizi c9d328df4a Update README.md 2023-10-10 10:41:21 +03:30
Meysam Parvizi 8f38139a77 Update README.md 2023-10-09 11:23:35 +03:30
Meysam Parvizi b6af1949e4 Update README.md 2023-10-05 19:52:19 +03:30
Meysam Parvizi 4661930a8d Update README.md 2023-10-04 15:24:27 +03:30
Meysam Parvizi b8f0a07be3 Update README.md 2023-10-03 11:00:01 +03:30
Meysam Parvizi 0becbff1e7 Update README.md (#9) 2023-10-02 22:44:07 +03:30
Meysam Parvizi 6766c7a35f Merge pull request #10 from danielinux/master
Updated README.md
Added Embedded Systems Architecture by Daniele Lacamera
2023-10-02 22:22:16 +03:30
Daniele Lacamera f92931cd7f Updated README.md 2023-10-02 20:39:30 +02:00
Meysam Parvizi 66f790152f Update README.md 2023-10-02 15:43:55 +03:30
Meysam Parvizi a3afd8be59 Update README.md 2023-10-02 05:06:19 +03:30
Meysam Parvizi 02d6f0e064 Update README.md 2023-10-01 18:35:43 +03:30
Meysam Parvizi eec80ce80c Update README.md 2023-09-30 18:02:25 +03:30
Meysam Parvizi 0c220e1eb8 Update README.md 2023-09-30 17:58:31 +03:30
Meysam Parvizi 182d258870 Update README.md 2023-09-30 17:52:17 +03:30
Meysam Parvizi 6808191db9 Update README.md 2023-09-30 16:25:10 +03:30
Meysam Parvizi 7e38537e05 Change titles in Electronics section 2023-09-30 15:51:51 +03:30
Meysam Parvizi d8700a5775 Update README.md 2023-09-30 13:51:14 +03:30
Meysam Parvizi a4b4a045c7 Update README.md 2023-09-27 19:11:48 +03:30
Meysam Parvizi 2773b42314 Update README.md 2023-09-25 17:46:52 +03:30
Meysam Parvizi 58292445b1 Update README.md 2023-09-22 19:04:14 +03:30
Meysam Parvizi c8fb92948a Update README.md 2023-09-22 18:54:22 +03:30
Meysam Parvizi 41ff4521e6 Update README.md 2023-09-22 13:10:27 +03:30
Meysam Parvizi 56840a6664 Update README.md 2023-09-22 12:56:35 +03:30
Meysam Parvizi 1bff1a0686 Update README.md 2023-09-22 12:52:31 +03:30
Meysam Parvizi afcbe99a98 Update README.md 2023-09-22 11:55:34 +03:30
Meysam Parvizi 5d406bab42 Update README.md 2023-09-22 11:35:39 +03:30
Meysam Parvizi fc45b8b444 Update README.md 2023-09-22 10:14:38 +03:30
Meysam Parvizi 8aa09359cf Update README.md 2023-09-21 19:48:07 +03:30
Meysam Parvizi af04df26b8 Minor changes 2023-09-21 19:38:57 +03:30
Meysam Parvizi da1ca7233b Update README.md 2023-09-21 19:17:03 +03:30
Meysam Parvizi 7e84e77ba3 Update README.md 2023-09-21 18:52:58 +03:30
Meysam Parvizi 19ef9340e3 Update README.md 2023-09-21 18:43:55 +03:30
Meysam Parvizi c4d20e4845 Update README.md 2023-09-21 12:59:44 +03:30
Meysam Parvizi c069ace61d Update README.md 2023-09-20 22:56:26 +03:30
Meysam Parvizi 04429f60f4 Update README.md 2023-09-20 21:11:16 +03:30
Meysam Parvizi e7eff402db Update README.md 2023-09-20 19:06:47 +03:30
Meysam Parvizi c2ea2585c7 Update README.md 2023-09-19 20:25:11 +03:30
Meysam Parvizi ffc094f1ce Update README.md 2023-09-19 18:26:48 +03:30
Meysam Parvizi 2c71dffe3e Update README.md 2023-09-19 16:29:40 +03:30
Meysam Parvizi bbf7e9e1db Update README.md 2023-09-19 15:52:48 +03:30
Meysam Parvizi b290eb068b Update README.md 2023-09-18 16:31:30 +03:30
Meysam Parvizi a614ca5455 Update README.md 2023-09-18 16:20:24 +03:30
Meysam Parvizi 7897f021e2 Update README.md 2023-09-18 16:15:52 +03:30
Meysam Parvizi fddf1a0b4a Update README.md 2023-09-17 23:57:38 +03:30
Meysam Parvizi 303cb479ab Update README.md 2023-09-16 15:31:45 +03:30
Meysam Parvizi 127be1e92d Update README.md 2023-09-16 13:53:12 +03:30
Meysam Parvizi d3be928502 Update README.md 2023-09-15 23:51:33 +03:30
Meysam Parvizi bc37c8f0ab Update README.md 2023-09-15 23:49:58 +03:30
Meysam Parvizi c443bab698 Update README.md 2023-09-15 22:16:02 +03:30
Meysam Parvizi 99ad98a2ee Update README.md 2023-09-15 07:11:18 +03:30
Meysam Parvizi 740d39a4ff Make list of RTOSes shorter
Remove 1-Wire
Add Thread
2023-09-15 06:49:31 +03:30
Meysam Parvizi d12fed56d4 Update README.md 2023-09-15 02:25:56 +03:30
Meysam Parvizi c0281bf33b Update README.md 2023-09-14 19:38:08 +03:30
Meysam Parvizi df23e2d927 Update README.md 2023-09-13 13:58:46 +03:30
Meysam Parvizi bd956b1743 Update README.md
Add some new resources
2023-09-13 12:46:02 +03:30
Meysam Parvizi 5ae5a7336a Update README.md 2023-09-13 12:22:39 +03:30
Meysam Parvizi 5137385766 Merge pull request #8 from kaizoku-oh/emilation/renode
Add resources for `Renode`
2023-09-13 12:14:06 +03:30
Bayrem Gharsellaoui ed21862677 Merge branch 'master' into emilation/renode 2023-09-13 09:32:35 +01:00
Meysam Parvizi f884c71825 Update README.md
Add "(YouTube Playlist)" suffix to all YouTube playlist links
2023-09-12 01:45:23 +03:30
Meysam Parvizi a969b64f4a Update README.md
Add `Renode`
2023-09-12 01:24:14 +03:30
Meysam Parvizi 4a921c486c Update README.md 2023-09-12 00:51:32 +03:30
Meysam Parvizi 24676992a8 Update README.md 2023-09-11 20:35:42 +03:30
Meysam Parvizi ef0e2e4a88 Update README.md 2023-09-11 15:10:57 +03:30
Meysam Parvizi 8a43d03ff5 Update README.md 2023-09-11 15:07:51 +03:30
Bayrem Gharsellaoui e963933d68 Adding renode to Simulation / Emulation Tools 2023-09-11 12:16:13 +01:00
Meysam Parvizi a20e4aca35 Fix empty box of Simulation / Emulation Tools 2023-09-11 14:36:46 +03:30
Meysam Parvizi 997e6f7b09 Add Simulation / Emulation Tools
Rearrange items in`Operatin Sytems` section
Adopt Semantic Versioning
2023-09-11 14:25:43 +03:30
Meysam Parvizi 2539b6f484 Update README.md 2023-09-10 15:15:57 +03:30
Meysam Parvizi 4ba2d4a1e3 Update README.md 2023-09-10 11:43:25 +03:30
Meysam Parvizi 20b620c321 Update README.md 2023-09-10 01:04:31 +03:30
Meysam Parvizi 1c59b3c8f0 Update README.md
Add QEMU resources
2023-09-10 01:04:03 +03:30
Meysam Parvizi 05e9d836fe Update README.md 2023-09-09 14:43:01 +03:30
Meysam Parvizi f84f0320b1 Update README.md 2023-09-07 17:07:23 +03:30
Meysam Parvizi 73e3af2d57 Update README.md
Add SDIO
2023-09-06 15:16:29 +03:30
Meysam Parvizi c0434b7d60 Update README.md 2023-09-06 03:13:00 +03:30
Meysam Parvizi 18bd3a9abe Update README.md 2023-09-05 12:57:15 +03:30
Meysam Parvizi 8f8f31b9b4 Update README.md 2023-09-04 12:31:20 +03:30
Meysam Parvizi 0ace66511f Update README.md 2023-09-03 20:50:59 +03:30
Meysam Parvizi fb94514cf4 Update README.md 2023-09-03 15:39:08 +03:30
Meysam Parvizi 73e7edbe82 Update README.md
Add RTOS doc links
2023-09-03 15:27:37 +03:30
Meysam Parvizi 24039054bf Update README.md 2023-09-03 14:53:25 +03:30
Meysam Parvizi f25a3e7930 Update README.md 2023-09-02 14:54:43 +03:30
Meysam Parvizi 3e0f30c886 Update README.md 2023-08-31 15:33:00 +03:30
Meysam Parvizi ece884be57 Update README.md 2023-08-30 00:09:07 +03:30
Meysam Parvizi 7608392b2d Update README.md 2023-08-29 23:23:03 +03:30
Meysam Parvizi b97beec23e Update README.md 2023-08-29 21:47:28 +03:30
Meysam Parvizi 7b7698aede Add Clock Management 2023-08-29 18:42:28 +03:30
Meysam Parvizi 249d971baf Update README.md 2023-08-29 17:54:25 +03:30
Meysam Parvizi ccc80662ce Update README.md 2023-08-29 16:18:33 +03:30
Meysam Parvizi 728ee3137c Update README.md 2023-08-29 16:04:26 +03:30
Meysam Parvizi c09b48e5f9 Update README.md
Add resources for Testing
2023-08-29 11:39:03 +03:30
Meysam Parvizi cef2989c21 Minor changes 2023-08-29 10:35:28 +03:30
Meysam Parvizi ae60ba18d3 Update README.md 2023-08-29 10:10:06 +03:30
Meysam Parvizi 217af4bbbb Update README.md 2023-08-28 19:49:10 +03:30
Meysam Parvizi 51a62f7f70 Update README.md 2023-08-28 19:37:52 +03:30
Meysam Parvizi c33a104269 Update README.md
Add resources to Sensors & Actuators
2023-08-28 19:07:15 +03:30
Meysam Parvizi 0b656b23dd Update README.md
Add Azure RTOS
2023-08-27 23:19:27 +03:30
Meysam Parvizi bf4cd0a1aa Update README.md 2023-08-27 21:45:24 +03:30
Meysam Parvizi 2207d1145a Update README.md 2023-08-27 21:20:19 +03:30
Meysam Parvizi 9847c8b7b3 Update README.md
Add some resources for Embedded Linux
2023-08-27 19:07:43 +03:30
Meysam Parvizi 7b92c127e4 Update README.md
Add more defintions for embedded system
2023-08-27 18:40:55 +03:30
Meysam Parvizi d4ab19536c Update README.md 2023-08-27 13:10:15 +03:30
Meysam Parvizi 2d7b6f2a23 Update README.md 2023-08-27 13:01:00 +03:30
Meysam Parvizi 94468297c2 Update README.md 2023-08-27 12:19:54 +03:30
Meysam Parvizi c7a06d423f Update README.md 2023-08-27 11:28:17 +03:30
Meysam Parvizi c00391832c Update README.md 2023-08-27 10:51:22 +03:30
Meysam Parvizi eccff74869 Update README.md
Add `VSCode Extensions`
2023-08-27 10:38:16 +03:30
Meysam Parvizi e094f76eba Update README.md 2023-08-27 09:36:43 +03:30
Meysam Parvizi 4620038aaa Update README.md
Add `The Linux Kernel` online book
2023-08-27 09:27:52 +03:30
Meysam Parvizi 274413eab2 Update README.md 2023-08-27 00:17:51 +03:30
Meysam Parvizi 5e8ebd6a95 Update README.md
Add lwIP
2023-08-27 00:01:39 +03:30
Meysam Parvizi 27478f814d Update README.md 2023-08-25 19:12:36 +03:30
Meysam Parvizi b301601e4d Update README.md 2023-08-25 19:09:18 +03:30
Meysam Parvizi ab6f8f4530 Update README.md 2023-08-25 19:01:18 +03:30
Meysam Parvizi 3a84ada859 Update README.md 2023-08-25 18:48:02 +03:30
Meysam Parvizi 56af45a87f Update README.md 2023-08-25 18:44:20 +03:30
Meysam Parvizi 4b006348a9 Update README.md 2023-08-24 12:05:11 +03:30
Meysam Parvizi fb673ba8f6 Update README.md
Add IDEs
2023-08-24 12:01:28 +03:30
Meysam Parvizi bed4b9bce6 Update README.md 2023-08-24 11:35:12 +03:30
Meysam Parvizi b9a133446f Update README.md 2023-08-23 16:46:05 +03:30
Meysam Parvizi cb966fb251 Minor change 2023-08-23 16:43:42 +03:30
Meysam Parvizi 20c8f8cc29 Add CI/CD Flow
Make `OS fundamentals` as required
2023-08-23 16:36:08 +03:30
Meysam Parvizi a59bd2cb43 Update README.md 2023-08-23 15:50:25 +03:30
Meysam Parvizi b28b7727dc Update README.md 2023-08-20 01:05:39 +03:30
Meysam Parvizi 52e857b600 Update README.md 2023-08-19 09:21:14 +03:30
Meysam Parvizi 5abfa08d9c Update README.md 2023-08-18 16:57:33 +03:30
Meysam Parvizi 6834f00c8a Update README.md 2023-08-17 14:12:25 +03:30
Meysam Parvizi 12f2d5f0f0 Update README.md 2023-08-16 19:50:24 +03:30
Meysam Parvizi 2f3e688e8e Update README.md 2023-08-16 18:58:28 +03:30
Meysam Parvizi 3aa4cd5554 Update README.md 2023-08-16 14:27:17 +03:30
Meysam Parvizi 77546b3d26 Update README.md 2023-08-16 14:23:28 +03:30
Meysam Parvizi fae8ddcc4a Update README.md
Fix typos
2023-08-16 14:15:54 +03:30
Meysam Parvizi 4dc83b5e73 Update README.md 2023-08-15 19:52:40 +03:30
Meysam Parvizi 4c1be44151 Update README.md
Add Bare-Metal vs RTOS
2023-08-15 15:57:15 +03:30
Meysam Parvizi 92228a79a9 Update README.md 2023-08-15 14:59:42 +03:30
Meysam Parvizi 6dd86addb4 Update README.md
Add 'The Definitive Guide to ARM' books
2023-08-15 01:38:37 +03:30
Meysam Parvizi 69012842fc Update README.md 2023-08-14 22:53:17 +03:30
Meysam Parvizi 8a00f9f519 Update README.md 2023-08-14 22:14:31 +03:30
Meysam Parvizi d64d8fdfb4 Update README.md 2023-08-14 22:10:37 +03:30
Meysam Parvizi ca19b1bb7c Update README.md
Add resources for Embedded Linux Security
2023-08-14 16:24:11 +03:30
Meysam Parvizi 19f2f2b0cb Update README.md
Add some resources for C++
2023-08-13 19:06:31 +03:30
Meysam Parvizi 7ceee1f7c5 Update README.md 2023-08-13 04:27:03 +03:30
Meysam Parvizi 140697feb5 Update README.md
Add TinyML book
2023-08-13 03:18:14 +03:30
Meysam Parvizi 98c52b4c46 Update README.md 2023-08-13 02:07:07 +03:30
Meysam Parvizi f2c659ea95 Update README.md 2023-08-13 01:16:11 +03:30
Meysam Parvizi c3ec72d8c7 Update README.md
Add Coursera and edX courses
2023-08-13 01:15:26 +03:30
Meysam Parvizi 88243467d6 Minorest minor change 2023-08-12 17:21:40 +03:30
Meysam Parvizi f57a1baac6 Update README.md 2023-08-12 17:16:27 +03:30
Meysam Parvizi 6b210b76f6 Update README.md
Add GoogleTest
2023-08-12 17:09:33 +03:30
Meysam Parvizi 97885a21c3 Update README.md
Add some resources for CAN
2023-08-12 17:08:08 +03:30
Meysam Parvizi e962f25474 Update README.md
Add U8g2
2023-08-12 17:02:49 +03:30
Meysam Parvizi 97d0dfb407 Minor changes 2023-08-12 13:31:59 +03:30
Meysam Parvizi c16eb7945f Update README.md 2023-08-12 13:11:26 +03:30
Meysam Parvizi 5c9b3ca37b Update README.md 2023-08-12 12:13:35 +03:30
Meysam Parvizi bb125e66ff Update README.md
Add RS485 and RS232
2023-08-12 01:42:12 +03:30
Meysam Parvizi c975a2579c Update README.md 2023-08-12 01:36:55 +03:30
Meysam Parvizi 33d363bf0b Update README.md
Add resources for Cryptography
2023-08-12 01:26:52 +03:30
Meysam Parvizi b9570fdbf3 Update README.md
Add TPM
2023-08-11 15:29:27 +03:30
Meysam Parvizi fe25902b4a Update README.md
Add Networking Fundamentals
2023-08-11 15:12:52 +03:30
Meysam Parvizi bd04036cda Update README.md 2023-08-11 13:24:57 +03:30
Meysam Parvizi 79b6c2427c Update README.md 2023-08-11 12:55:44 +03:30
Meysam Parvizi 98137ec87c Update README.md 2023-08-11 12:40:07 +03:30
Meysam Parvizi ea4ed18fbf Update README.md
Add FreeRTOS vs Linux for Embedded Systems
2023-08-11 12:39:22 +03:30
Meysam Parvizi 83143802c9 Update README.md
Add horizontal rules
2023-08-11 12:34:32 +03:30
Meysam Parvizi 9e58c68f92 Update README.md
Add resources for Integration Testing
2023-08-11 12:23:58 +03:30
Meysam Parvizi 7f0d0bb032 Update README.md
Add resources for Cellular technologies
2023-08-11 12:17:27 +03:30
Meysam Parvizi 9ae858a12a Update README.md 2023-08-11 03:25:21 +03:30
Meysam Parvizi 9855357fd2 Update README.md 2023-08-10 23:33:41 +03:30
Meysam Parvizi 5a61f48526 Update README.md
Add `Interrupt Blog by Memfault`
2023-08-10 18:46:54 +03:30
Meysam Parvizi b29ae26ce8 Update README.md
Add `SDRAM in STM32 || MT48LC4`
2023-08-10 12:19:43 +03:30
Meysam Parvizi 60b95f9542 Update README.md
Add `PCB Design Skill Tree`
2023-08-10 12:18:09 +03:30
Meysam Parvizi 3f5a71258a Update README.md
Add `Embedded Systems Skill Tree`
2023-08-10 12:16:44 +03:30
Meysam Parvizi 205e637110 Update README.md
Add Practical System Programming with C
2023-08-10 02:47:23 +03:30
Meysam Parvizi 8af3b6dcbc Update README.md 2023-08-10 01:48:45 +03:30
Meysam Parvizi e20d634ffa Update README.md
Add SafeRTOS
2023-08-10 01:29:49 +03:30
Meysam Parvizi c89cf2a40f Update README.md 2023-08-10 01:23:45 +03:30
Meysam Parvizi bba97a7664 Update README.md
Add resources for protocols
2023-08-10 01:23:04 +03:30
Meysam Parvizi f77f545118 Update README.md
Add 'The Future of Programming: Rust'
2023-08-09 20:22:42 +03:30
Meysam Parvizi e1c3c01126 Update README.md
Add Hardware Hacking
2023-08-09 20:19:04 +03:30
Meysam Parvizi 0ba36a7505 Update README.md
Add definition of Embedded System
2023-08-09 19:40:04 +03:30
Meysam Parvizi 88fdfe6379 Update README.md 2023-08-09 17:12:31 +03:30
Meysam Parvizi 1147c50066 Update README.md 2023-08-09 17:07:35 +03:30
Meysam Parvizi 2348b365a8 Update README.md 2023-08-09 16:25:09 +03:30
Meysam Parvizi 3f456aea92 Update README.md
Add resources for Multitasking and Parallel Processing
2023-08-09 12:40:00 +03:30
Meysam Parvizi 52b9f41521 Update README.md 2023-08-09 11:05:30 +03:30
Meysam Parvizi edcd4a5342 Update README.md 2023-08-09 04:16:08 +03:30
Meysam Parvizi 2ea0b0aba7 Update README.md 2023-08-09 01:21:44 +03:30
Meysam Parvizi e121a8e160 Update README.md 2023-08-09 01:19:33 +03:30
Meysam Parvizi 0b4f7483d9 Minor changes 2023-08-08 22:33:36 +03:30
Meysam Parvizi ecf6afad3a Add some wireless protocols
Add μC/OS
2023-08-08 22:13:47 +03:30
Meysam Parvizi c50c0579b1 Update README.md 2023-08-08 21:02:40 +03:30
Meysam Parvizi c2dbf403e5 Update README.md
Add μC/OS / Micriμm OS
2023-08-08 20:49:58 +03:30
Meysam Parvizi cf4c1f4384 Update README.md 2023-08-08 12:40:43 +03:30
Meysam Parvizi 4a1dfa5058 Update README.md 2023-08-08 11:58:08 +03:30
Meysam Parvizi 65ba823ff1 Add SDLC Models 2023-08-08 11:47:16 +03:30
Meysam Parvizi 2e6c8bd366 Update README.md 2023-08-08 01:59:23 +03:30
Meysam Parvizi c5bc921acf Update README.md 2023-08-08 01:33:13 +03:30
Meysam Parvizi 639a5f9087 Update README.md 2023-08-08 01:22:38 +03:30
Meysam Parvizi 2c7a23f7c0 Update README.md 2023-08-07 19:17:06 +03:30
Meysam Parvizi 087f30af52 Update README.md 2023-08-07 16:58:53 +03:30
Meysam Parvizi 1b9968e8cc Update README.md
Add Udemy prefix to courses
2023-08-07 16:48:12 +03:30
Meysam Parvizi 7e9e42965b Update README.md 2023-08-07 15:15:50 +03:30
Meysam Parvizi 21d63623ff Update README.md 2023-08-07 15:08:34 +03:30
Meysam Parvizi 6bb25ba5c3 Update README.md 2023-08-07 02:55:28 +03:30
Meysam Parvizi 9ab7761c89 Update README.md 2023-08-07 02:23:37 +03:30
Meysam Parvizi 6f7e998db6 Update README.md 2023-08-07 02:04:05 +03:30
Meysam Parvizi 3227c98762 Update README.md 2023-08-06 19:48:05 +03:30
Meysam Parvizi 2d6523dd8f Update README.md
Add some books
2023-08-06 19:32:18 +03:30
Meysam Parvizi 9b45029902 Update README.md
Add SOFT SKILLS
2023-08-06 18:45:57 +03:30
Meysam Parvizi 812195e7c5 Update README.md 2023-08-06 14:28:24 +03:30
Meysam Parvizi 7aa5ee7c9f Update README.md 2023-08-06 14:27:11 +03:30
Meysam Parvizi efe86eb64e Add Hardware Design Basics 2023-08-06 13:53:05 +03:30
Meysam Parvizi d9347c07cd Update README.md 2023-08-06 13:50:36 +03:30
Meysam Parvizi 2913f8d0c4 Update README.md 2023-08-06 13:49:29 +03:30
Meysam Parvizi 4f93d73ba7 Update README.md 2023-08-06 13:48:33 +03:30
Meysam Parvizi fa48729090 Update README.md
Add many useful links
2023-08-06 13:46:41 +03:30
Meysam Parvizi a8ea3d037a Update README.md
Add some links
2023-08-04 21:49:43 +03:30
Meysam Parvizi c2337f54d4 Update README.md 2023-08-04 20:41:55 +03:30
Meysam Parvizi 4a52aab313 Update README.md
Add note about Arduino
2023-08-04 20:03:19 +03:30
Meysam Parvizi f5dee15b72 Update README.md
Add CAN
2023-08-04 19:28:49 +03:30
Meysam Parvizi 71cac1f090 Update README.md
Add Qt Documentation
2023-08-04 14:56:11 +03:30
Meysam Parvizi aa1d88863c Update README.md 2023-08-04 13:28:01 +03:30
Meysam Parvizi 1f8ef79660 Update README.md 2023-08-04 13:17:12 +03:30
Meysam Parvizi 0bd7a541ec Update README.md 2023-08-04 12:25:55 +03:30
Meysam Parvizi 52a103d493 Update README.md 2023-08-04 11:51:50 +03:30
Meysam Parvizi d4e11f4952 Merge pull request #7 from hlord2000/cellular_info
Added references to nRF devacademy courses
2023-08-04 11:47:11 +03:30
hlord2000 3b7fb11dbc Added references to nRF devacademy courses 2023-08-03 23:22:50 -04:00
Meysam Parvizi 70467be709 Update README.md 2023-08-03 17:00:31 +03:30
Meysam Parvizi 7c89a8d994 Update README.md 2023-08-03 16:46:08 +03:30
Meysam Parvizi a974623490 Minor change 2023-08-03 16:43:49 +03:30
Meysam Parvizi 02d3600506 Update README.md 2023-08-03 16:42:13 +03:30
Meysam Parvizi 8ec15c8825 Minor changes
Remove the term "Industry Ready"
2023-08-03 16:00:27 +03:30
Meysam Parvizi 11b4c8e638 Update README.md 2023-08-03 15:31:18 +03:30
Meysam Parvizi 6f13c90f7d Update README.md 2023-08-02 14:48:29 +03:30
Meysam Parvizi 6e79f9f1b3 Update README.md 2023-08-02 13:27:54 +03:30
Meysam Parvizi 7783fcb88f Minor changes 2023-08-02 01:56:32 +03:30
Meysam Parvizi 19d2d522e9 Update README.md 2023-08-02 01:24:57 +03:30
Meysam Parvizi 81a1d2d03d Update README.md
Add Security & Cryptography
2023-08-02 01:17:09 +03:30
Meysam Parvizi c77f3fc47c Update README.md 2023-08-02 00:01:11 +03:30
Meysam Parvizi 3d90a4f057 Update README.md 2023-08-01 23:26:58 +03:30
Meysam Parvizi ce4a816095 Update README.md 2023-08-01 21:03:35 +03:30
Meysam Parvizi 765f809d82 Update README.md
Add Digital Signal Processing
2023-08-01 20:54:05 +03:30
Meysam Parvizi e86c2a3d6d Update README.md
Add RT-Thread
2023-08-01 13:14:05 +03:30
Meysam Parvizi a6f015a751 Update README.md
Update Embedded AI
2023-08-01 03:52:26 +03:30
Meysam Parvizi e83d205475 Update README.md 2023-07-31 21:50:53 +03:30
Meysam Parvizi 651d69b0a2 Update README.md
Add FreeRTOS and Zephyr
2023-07-31 21:48:07 +03:30
Meysam Parvizi 7ca183d679 Update README.md
Add Bootloader
2023-07-31 21:38:27 +03:30
Meysam Parvizi 7dd228bf51 Update README.md
Add all roadmap topics
2023-07-31 13:52:25 +03:30
Meysam Parvizi 735f309232 Update README.md 2023-07-31 13:01:56 +03:30
Meysam Parvizi 67e056564e Update README.md 2023-07-31 12:57:22 +03:30
Meysam Parvizi f691a05766 Minor changes 2023-07-31 12:53:39 +03:30
Meysam Parvizi 83e3d585f1 Update README.md 2023-07-31 12:52:50 +03:30
Meysam Parvizi abdea95830 Update README.md 2023-07-31 12:01:59 +03:30
Meysam Parvizi 78981869e5 Update README.md 2023-07-31 11:59:49 +03:30
Meysam Parvizi cea9490309 Minor change 2023-07-31 11:45:20 +03:30
Meysam Parvizi 40ec98eb6c Update README.md 2023-07-31 11:43:32 +03:30
Meysam Parvizi 206c129f9f Update README.md
Add Version Control
2023-07-31 11:30:32 +03:30
Meysam Parvizi 75eb9c5f39 Update README.md
Add Build System
2023-07-31 11:11:56 +03:30
Meysam Parvizi 1cfad9c59a Update README.md 2023-07-31 03:13:20 +03:30
Meysam Parvizi 6313c63883 Update Roadmap
Add 'Embedded AI' 
Add 'UML' 
Add 'VxWorks' 
Add 'Qt'
2023-07-31 02:48:38 +03:30
Meysam Parvizi cf21153c09 Update README.md 2023-07-31 01:47:04 +03:30
Meysam Parvizi f41e5d3738 Update README.md
Add Programming Languages
2023-07-31 01:25:19 +03:30
Meysam Parvizi a016955502 Update README.md
Add Algorithms & Data Structures
2023-07-30 23:31:09 +03:30
Meysam Parvizi bf22f434ef Update README.md 2023-07-30 22:33:49 +03:30
Meysam Parvizi 42d3f0e325 Update README.md 2023-07-30 21:54:17 +03:30
Meysam Parvizi 65da788e53 Add resources for EMI / SI in PCB design 2023-07-30 13:58:44 +03:30
Meysam Parvizi a7bd324f45 Update README.md 2023-07-30 13:10:07 +03:30
Meysam Parvizi 23c671da4e Update README.md 2023-07-30 13:07:45 +03:30
Meysam Parvizi b1dce8cb2b Add 'The Art of Electronics' 2023-07-30 13:06:58 +03:30
Meysam Parvizi a9a9fc5f02 Update README.md 2023-07-30 13:00:25 +03:30
Meysam 37b3fc63f4 Add 'Encyclopedia of Electronic Components' 2023-07-30 12:51:36 +03:30
Meysam 2095451d43 Add some resources for hardware basics 2023-07-30 12:48:34 +03:30
Meysam c38b928765 Add Ben Eater 8-bit breadboard (#6) 2023-07-30 11:11:48 +03:30
Meysam 9bb1700f2a Add some Learning Resources 2023-07-30 00:05:26 +03:30
Meysam b9d63a0a26 Update README.md 2023-07-29 22:44:53 +03:30
Meysam 1235564886 Add 'Organizational & Time Management Skills' 2023-07-29 22:44:13 +03:30
Meysam d2f0f8fb47 Update README.md 2023-07-29 22:05:05 +03:30
Meysam 96ba2bae7e Add Docker (#5)
Add CoAP & cellular networks (#4)
Add Secure Boot and OTA (#3)
Add Functional Safety
Add MATLAB / Simulink
2023-07-29 16:51:59 +03:30
Meysam 5e33251915 Add SOFT SKILLS (#2) 2023-07-29 14:04:20 +03:30
Meysam e947b65b2d Minor changes 2023-07-28 22:05:31 +03:30
Meysam 21b7ed7e54 Update README.md 2023-07-28 21:48:16 +03:30
Meysam eb50a7f92e More emphasize on DSP
Add eMMC memory
Add Rework
2023-07-28 21:42:13 +03:30
Meysam cae6215eb7 Update README.md 2023-07-28 15:11:06 +03:30
Meysam c734253b08 Update README.md 2023-07-28 14:37:05 +03:30
Meysam 5c0d7b2e90 Update README.md 2023-07-28 13:35:25 +03:30
Meysam 5d222a22b8 Update README.md 2023-07-27 20:44:44 +03:30
Meysam 41ef7f0986 Remove 'Architectures' section
Edit "Protocols" section
Remove "Binary Arithmetic" (already included in "Logic Design"
Add "Memory Management"
Fix notes
2023-07-27 20:40:34 +03:30
Meysam f0cebd35d8 Update README.md 2023-07-27 13:14:44 +03:30
Meysam df09e07d27 Add Bootloder
Add some notes
Remove SVN
2023-07-27 13:12:28 +03:30
Meysam 8b3ea9dd7f Update README.md 2023-07-27 00:52:55 +03:30
Meysam ab85827815 Update README.md 2023-07-27 00:48:43 +03:30
Meysam 5ef7e0a988 Reorganize the diagram
Remove `Development Methodologies`
2023-07-27 00:43:34 +03:30
Meysam d0e1c59fd9 Minor changes 2023-07-26 22:32:29 +03:30
Meysam f8c4c1ab63 Update README.md 2023-07-26 22:27:24 +03:30
Meysam 5588af02d8 Add a note about FPGAs 2023-07-26 22:22:53 +03:30
Meysam f622f845ef Add Power Management 2023-07-26 18:06:36 +03:30
Meysam b9eab2708b Update README.md 2023-07-26 16:44:54 +03:30
Meysam 7933f5bc3b Add DSP and Cryptography sections.
Edit Electronics Fundamentals
2023-07-26 16:35:44 +03:30
Meysam 94b0bc9a36 Dotted arrow lines 2023-07-26 11:41:30 +03:30
Meysam 935eafffc0 Update README.md 2023-07-26 11:25:58 +03:30
Meysam 93d58293b3 Update README.md 2023-07-26 11:09:12 +03:30
Meysam aa0dfda62a Update README.md 2023-07-26 10:55:45 +03:30
34 changed files with 6440 additions and 8 deletions
+1
View File
@@ -0,0 +1 @@
custom: ['https://www.buymeacoffee.com/m3y54m']
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Contributing guidelines
url: https://github.com/m3y54m/Embedded-Engineering-Roadmap/blob/master/CONTRIBUTING.md
about: Read these before suggesting a resource or opening a pull request.
@@ -0,0 +1,19 @@
name: Report a link problem
description: A link is broken, outdated, mislabeled or in the wrong section
title: "[Link] "
labels: ["bug"]
body:
- type: input
id: entry
attributes:
label: Which link?
description: The title or URL of the entry.
validations:
required: true
- type: textarea
id: problem
attributes:
label: What is wrong, and what should happen to it?
description: For example "returns 404, replace with <link>" or "belongs under Debugging".
validations:
required: true
@@ -0,0 +1,26 @@
name: Suggest a resource
description: Propose a link to add to the roadmap
title: "[Resource] "
labels: ["enhancement"]
body:
- type: input
id: url
attributes:
label: Link
placeholder: https://
validations:
required: true
- type: input
id: section
attributes:
label: Where does it belong?
description: The README section, for example "Microcontrollers > Interrupts".
validations:
required: true
- type: textarea
id: why
attributes:
label: Why add it?
description: One or two sentences. Mention if it is paid or aimed at beginners.
validations:
required: true
+7
View File
@@ -0,0 +1,7 @@
# What changed
<!-- Added, moved or removed entries, and the section -->
- [ ] Entries follow the format and markers described in the README
- [ ] Each link works and matches its section
- [ ] Each addition has a clear reason (free resources preferred; see [CONTRIBUTING.md](../CONTRIBUTING.md))
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
+28
View File
@@ -0,0 +1,28 @@
# Many sites in the README (Udemy, Medium, Microchip, ...) answer bots with 403/429 while
# working fine in a browser, and some redirect to consent pages, so these count as alive.
accept = ["200..=399", "403", "429"]
exclude = [
# Bot challenges or non-standard codes for every request.
'^https?://(www\.)?amazon\.',
'^https?://a\.co/',
'^https?://(www\.)?linkedin\.com/',
# Return 404 to non-browser clients although the pages exist.
'^https?://(www\.)?nxp\.com/',
# Fail HTTP/2 negotiation with the checker's client.
'^https?://(www\.|wiki\.)?st\.com/',
'^https?://(www\.)?analog\.com/',
'^https?://(www\.)?embedded\.com/',
'^https?://(www\.)?electronics-tutorials\.ws',
'^https?://gemini\.google\.com/',
# Broken certificate chain on the server side.
'^https?://(www\.)?autosar\.org/',
# Repository badge links; GitHub hides these pages from anonymous clients.
'^https://github\.com/m3y54m/Embedded-Engineering-Roadmap/(stargazers|watchers|forks|graphs|releases)',
]
max_retries = 3
retry_wait_time = 5
timeout = 60
max_concurrency = 8
user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Check README.md resource entries against the conventions in CONTRIBUTING.md."""
import re
import sys
from pathlib import Path
README = Path(__file__).resolve().parents[2] / "README.md"
# Type marker, then optional beginner (👶), then optional invaluable (💎).
ENTRY = re.compile(r"^- \[(?:📘|🎞️?|📝|🔗|🎧)👶?💎? .+\]\(https?://\S+\)")
LIST_LINK = re.compile(r"^- \[")
URL = re.compile(r"\]\((https?://[^)\s]+(?:\([^)\s]*\))?[^)\s]*)\)")
BAD_CHARS = {"\ufffd": "U+FFFD replacement character", "\u200b": "zero-width space",
"\u200c": "zero-width non-joiner"}
HEADING = re.compile(r"^#{2,6} ")
def main() -> int:
errors = []
section = ""
seen = {}
for number, line in enumerate(README.read_text(encoding="utf-8").splitlines(), 1):
for char, name in BAD_CHARS.items():
if char in line:
errors.append(f"{number}: {name}")
if HEADING.match(line):
section = line
seen = {}
continue
if not LIST_LINK.match(line):
continue
if not ENTRY.match(line):
errors.append(f"{number}: entry does not match '- [<type>[👶][💎] Title](url)'")
continue
match = URL.search(line)
if match:
url = match.group(1)
if url in seen:
errors.append(f"{number}: duplicate URL in section, first used on line {seen[url]}")
seen[url] = number
for error in errors:
print(f"README.md:{error}")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Decides the roadmap version for a build.
# - Patch releases are automatic: a new one is due when the rendered map differs from the latest release's.
# - Minor and major releases only happen when a maintainer asks for them (BUMP=minor|major).
# Inputs: FINGERPRINT (sha256 of the map PNG rendered without the revision line), BUMP (auto|minor|major),
# GH_REPO and GH_TOKEN for the GitHub API. Outputs (to $GITHUB_OUTPUT): release, version, previous, date.
set -euo pipefail
: "${FINGERPRINT:?}" "${GH_REPO:?}"
BUMP="${BUMP:-auto}"
tag=v0.0.0 published="" body=""
if info="$(gh api "repos/$GH_REPO/releases/latest" --jq '[.tag_name, .published_at[0:10]] | @tsv' 2>/dev/null)"; then
IFS=$'\t' read -r tag published <<<"$info"
body="$(gh api "repos/$GH_REPO/releases/latest" --jq '.body // ""')"
fi
previous_fingerprint="$(grep -oE 'map-fingerprint: [0-9a-f]{64}' <<<"$body" | cut -d' ' -f2 || true)"
if [[ ! $tag =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "::error::The latest release tag '$tag' is not in vMAJOR.MINOR.PATCH form."
exit 1
fi
major=${BASH_REMATCH[1]} minor=${BASH_REMATCH[2]} patch=${BASH_REMATCH[3]}
release=true
if (( major < 2 )); then
# Versions restart at 2.0.0 with the roadmap rendered from the site.
version=v2.0.0
elif [[ $BUMP == major ]]; then
version="v$((major + 1)).0.0"
elif [[ $BUMP == minor ]]; then
version="v$major.$((minor + 1)).0"
elif [[ $FINGERPRINT != "$previous_fingerprint" ]]; then
version="v$major.$minor.$((patch + 1))"
else
release=false
version=$tag
fi
date="$([[ $release == true ]] && date -u +%F || echo "$published")"
echo "Latest release $tag (map ${previous_fingerprint:-unknown}); this map $FINGERPRINT; bump $BUMP"
echo "=> release=$release version=$version date=$date"
{
echo "release=$release"
echo "version=$version"
echo "previous=$tag"
echo "date=$date"
} >> "${GITHUB_OUTPUT:-/dev/stdout}"
+44
View File
@@ -0,0 +1,44 @@
name: Link check
on:
schedule:
- cron: "0 6 * * 1"
pull_request:
paths:
- "**/*.md"
- ".github/lychee.toml"
workflow_dispatch:
permissions:
contents: read
issues: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lychee:
name: Check links
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Check links
id: lychee
uses: lycheeverse/lychee-action@v2
with:
args: --config .github/lychee.toml --no-progress "**/*.md"
output: lychee/out.md
# Pull requests report broken links without blocking; scheduled runs fail and open an issue.
fail: ${{ github.event_name != 'pull_request' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Report broken links
if: always() && github.event_name != 'pull_request' && steps.lychee.outputs.exit_code != 0
uses: peter-evans/create-issue-from-file@v6
with:
title: Link check report
content-filepath: lychee/out.md
labels: broken-links
+33
View File
@@ -0,0 +1,33 @@
name: Markdown lint
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
markdownlint:
name: Markdown lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: DavidAnson/markdownlint-cli2-action@v24
with:
globs: "**/*.md"
readme-conventions:
name: README conventions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Check entry format and duplicates
run: python3 .github/scripts/check_readme.py
+143
View File
@@ -0,0 +1,143 @@
name: Roadmap explorer
on:
push:
branches: [master]
paths:
- README.md
- "site/**"
- .github/workflows/pages.yml
- .github/scripts/next-version.sh
pull_request:
paths:
- README.md
- "site/**"
- .github/workflows/pages.yml
- .github/scripts/next-version.sh
workflow_dispatch:
inputs:
bump:
description: "Version bump: auto releases a patch only if the map changed; minor/major always release"
type: choice
options: [auto, minor, major]
default: auto
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Builds on master queue instead of cancelling, so two releases never pick the same version.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
name: Test and build
# Pinned runner and Playwright keep the rendering stable, so the map fingerprint only changes with the map.
runs-on: ubuntu-24.04
outputs:
release: ${{ steps.version.outputs.release }}
version: ${{ steps.version.outputs.version }}
previous: ${{ steps.version.outputs.previous }}
fingerprint: ${{ steps.fingerprint.outputs.sha256 }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Test the README parser and map layout
run: node --test site/parser.test.mjs
- name: Assemble the site
run: |
mkdir _site
cp site/*.html site/*.css site/*.js site/map.json README.md _site/
cp -r site/fonts site/assets _site/
- name: Install Chromium
run: |
npm install --no-save --no-package-lock playwright@1.64.0
npx playwright install --with-deps chromium
- name: Fingerprint the map
id: fingerprint
# Rendered before build.json exists, so the revision line is not part of the fingerprint.
run: |
node site/tools/render.mjs _site / fingerprint
echo "sha256=$(sha256sum fingerprint/Embedded-Engineering-Roadmap.png | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
- name: Decide the version
id: version
env:
FINGERPRINT: ${{ steps.fingerprint.outputs.sha256 }}
BUMP: ${{ inputs.bump || 'auto' }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: .github/scripts/next-version.sh
- name: Render the roadmap to PDF and PNG
env:
VERSION: ${{ steps.version.outputs.version }}
DATE: ${{ steps.version.outputs.date }}
run: |
node -e 'console.log(JSON.stringify({ version: process.env.VERSION, date: process.env.DATE }))' > _site/build.json
cat _site/build.json
node site/tools/render.mjs _site / _site
- uses: actions/upload-artifact@v4
with:
name: roadmap-render
path: |
_site/Embedded-Engineering-Roadmap.pdf
_site/Embedded-Engineering-Roadmap.png
if-no-files-found: error
- uses: actions/upload-pages-artifact@v3
with:
path: _site
release:
name: Release ${{ needs.build.outputs.version }}
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' && needs.build.outputs.release == 'true'
needs: build
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: roadmap-render
- name: Create the release with the PDF and PNG
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
VERSION: ${{ needs.build.outputs.version }}
PREVIOUS: ${{ needs.build.outputs.previous }}
FINGERPRINT: ${{ needs.build.outputs.fingerprint }}
run: |
gh api "repos/$GH_REPO/releases/generate-notes" -f tag_name="$VERSION" -f target_commitish="$GITHUB_SHA" \
-f previous_tag_name="$PREVIOUS" --jq .body > notes.md
# The next build compares its map with this fingerprint to decide whether a new patch release is due.
printf '\n<!-- map-fingerprint: %s -->\n' "$FINGERPRINT" >> notes.md
gh release create "$VERSION" Embedded-Engineering-Roadmap.pdf Embedded-Engineering-Roadmap.png \
--target "$GITHUB_SHA" --title "$VERSION" --notes-file notes.md
deploy:
name: Deploy to GitHub Pages
if: >-
!cancelled() && github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
&& needs.build.result == 'success' && needs.release.result != 'failure'
needs: [build, release]
runs-on: ubuntu-24.04
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
+11
View File
@@ -0,0 +1,11 @@
# Link checker cache
.lycheecache
# Editors and OS
.vscode/
.DS_Store
Thumbs.db
# Local tooling
node_modules/
dist/
+5
View File
@@ -0,0 +1,5 @@
{
"MD026": false,
"default": true,
"line-length": false
}
+41
View File
@@ -0,0 +1,41 @@
# Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [insert contact method here]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at [https://www.contributor-covenant.org/version/1/4/](https://www.contributor-covenant.org/version/1/4/).
For more details, visit the [Open Source Initiative](https://opensource.org/).
+42
View File
@@ -0,0 +1,42 @@
# Contributing
Contributions from everyone are welcomed. To keep the roadmap practical, accessible, and high quality, the following guidelines should be followed:
## 1. Prioritize Beginner-Friendliness
- Resources and explanations understandable for beginners should be added.
- Advanced or specialized topics are welcomed and their inclusion can enrich the roadmap, but they should be explicitly labeled as advanced so beginners are not overwhelmed when choosing resources.
## 2. Resource Selection Policy
- Free and open resources are preferred to maximize accessibility.
- Paid resources may be included only if they clearly offer more value than existing free options; low-quality paid/free content should not be promoted.
- The list should not be spammed with every publication from the same author or creator solely due to their reputation or personal preference. Each resource should be added for a clear reason, with usefulness especially for beginners prioritized.
- Resources should be up-to-date, reliable, and organized under relevant headings.
## 3. Clarity & Structure
- Clear and direct language should be used. Unnecessary jargon should be avoided. If technical terms are used, simple explanations should be added.
- Bullet points and lists should be used for readability and structure.
## 4. Technical Accuracy
- The correctness and relevance of all information and links should be verified.
- If uncertainty exists, feedback should be requested in the pull request.
## 5. Roadmap Alignment
- Contributions should match the topics and structure of the roadmap.
- Areas where contributors have experience or genuine interest should be focused on.
- If a new topic is thought to make the roadmap more complete, it may be suggested. New topics should be proposed thoughtfully, considering their usefulness and relevance for other learners.
## 6. Versioning and Releases
The roadmap image is versioned as `vMAJOR.MINOR.PATCH`, starting from `v2.0.0`.
- **No new version** for changes that do not change the roadmap map, such as new learning resources, descriptions or documentation.
- **Patch** (`v2.0.0` → `v2.0.1`) is automatic: when a change merged into `master` changes the rendered map, CI publishes the next patch release with the PDF and PNG attached.
- **Minor** (`v2.0.234` → `v2.1.0`) and **major** (`v2.43.57` → `v3.0.0`) are decided by the maintainer: run the *Roadmap explorer* workflow from the Actions tab on `master` and choose `minor` or `major`.
Releases should not be created by hand; the workflow renders the files and records the map fingerprint that later builds compare against.
Binary file not shown.
+1558 -8
View File
File diff suppressed because it is too large Load Diff
+668
View File
@@ -0,0 +1,668 @@
import { parseRoadmap, renderInline, TYPES } from './parser.js';
import { linkDiagram } from './diagram.js';
import { createMapView } from './mapview.js';
import { CREDITS, IMPORTANCE, areaDots, areaLabel, h, plural } from './dom.js';
const SOURCES = [
'README.md',
'../README.md',
'https://raw.githubusercontent.com/m3y54m/Embedded-Engineering-Roadmap/master/README.md',
];
const REPO_URL = 'https://github.com/m3y54m/Embedded-Engineering-Roadmap';
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
const narrowScreen = window.matchMedia('(max-width: 960px)');
const els = {
diagram: document.getElementById('diagram'),
outline: document.getElementById('outline'),
panel: document.getElementById('panel'),
status: document.getElementById('status'),
search: document.getElementById('search'),
results: document.getElementById('search-results'),
theme: document.getElementById('theme'),
tabs: [...document.querySelectorAll('[data-view]')],
};
const state = {
data: null,
selected: null,
includeSub: null,
filters: { types: new Set(), beginner: false, gem: false },
results: [],
active: -1,
};
// ---------- small helpers ----------
const normalize = (text) => text.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
function* descendants(node) {
yield node;
for (const child of node.children) yield* descendants(child);
}
function store(key, value) {
try {
if (value === undefined) return localStorage.getItem(key);
localStorage.setItem(key, value);
} catch {
// Storage can be unavailable (private mode, blocked cookies); preferences just won't persist.
}
return null;
}
// ---------- colors ----------
function assignHues(data) {
const [first, ...rest] = data.groups;
const start = rest.length ? first.topics : [];
const learning = rest.length ? rest.flatMap((g) => g.topics) : data.root.children;
start.forEach((t, i) => { t.hue = 168 + (start.length > 1 ? (i * 40) / (start.length - 1) : 0); });
learning.forEach((t, i) => { t.hue = (214 + (i * 300) / Math.max(1, learning.length)) % 360; });
data.root.children.forEach((top, index) => {
top.index = index;
for (const n of descendants(top)) n.top = top;
});
}
function colorOf(node) {
if (!node.top) return 'var(--accent)';
const dark = document.documentElement.dataset.theme !== 'light';
const step = Math.min(node.depth - 1, 3);
const alt = node.top.index % 2;
const light = dark ? [54, 44, 37, 32][step] + alt * 7 : [62, 73, 81, 87][step] - alt * 6;
const sat = dark ? [70, 56, 46, 40][step] : [72, 64, 56, 50][step];
return `hsl(${node.top.hue.toFixed(1)} ${sat}% ${light}%)`;
}
function swatch(node) {
const el = h('span', { class: 'swatch', 'aria-hidden': 'true' });
el.style.background = colorOf(node);
return el;
}
function truncate(text, max) {
if (text.length <= max) return text;
return max < 4 ? '' : `${text.slice(0, max - 1).trimEnd()}…`;
}
// ---------- navigation ----------
function navigate(node, { push = true, resource = null, reveal = false } = {}) {
if (state.selected !== node) state.includeSub = null;
state.selected = node;
if (resource) resetFilters(false);
if (state.mapView) state.mapView.select(node === state.data.root ? null : node);
renderPanel(node);
syncOutline();
if (resource) revealResource(resource);
const root = state.data.root;
document.title = node === root ? 'Embedded Engineering Roadmap · Explorer' : `${node.title} · Embedded Engineering Roadmap`;
if (push) {
const hash = node === root ? '#/' : `#/${node.slug}`;
if (location.hash !== hash) history.pushState(null, '', hash);
}
if (reveal && narrowScreen.matches) openSheet(true);
}
function routeFromHash() {
const slug = decodeURIComponent(location.hash.replace(/^#\/?/, ''));
navigate((slug && state.data.bySlug.get(slug)) || state.data.root, { push: false });
}
// ---------- details panel ----------
function section(title, ...content) {
return h('section', { class: 'panel-section' }, title ? h('h3', {}, title) : null, ...content);
}
function crumbs(node) {
const chain = [];
for (let n = node.parent; n; n = n.parent) chain.unshift(n);
if (!chain.length) return null;
const root = state.data.root;
return h('nav', { class: 'crumbs', 'aria-label': 'Topic path' }, chain.flatMap((n, i) => [
i ? h('span', { 'aria-hidden': 'true' }, '›') : null,
h('button', { type: 'button', onclick: () => navigate(n) }, n === root ? 'Overview' : n.title),
]));
}
function description(blocks) {
const wrap = h('div', { class: 'desc' });
for (const block of blocks) {
if (block.type === 'p') {
const p = h('p');
p.innerHTML = renderInline(block.text);
wrap.append(p);
} else {
const ul = h('ul');
for (const item of block.items) {
const li = h('li');
li.innerHTML = renderInline(item);
ul.append(li);
}
wrap.append(ul);
}
}
return wrap;
}
function topicChips(nodes) {
return h('div', { class: 'chips' }, nodes.map((n) => h('button', {
type: 'button',
class: 'chip-btn',
onclick: () => navigate(n, { reveal: true }),
}, swatch(n), n.title, h('span', { class: 'n' }, n.total))));
}
function related(node) {
const items = [...node.links]
.sort((a, b) => b.weight - a.weight)
.map((link) => {
const other = link.source === node ? link.target : link.source;
const reasons = [];
if (link.shared.length) reasons.push(plural(link.shared.length, 'shared resource'));
const outgoing = link.mentions.filter((m) => m.topic === node);
const incoming = link.mentions.filter((m) => m.topic !== node);
if (outgoing.length) reasons.push(`mentioned here: “${truncate(outgoing[0].text, 60)}”`);
else if (incoming.length) reasons.push(`mentions this topic: “${truncate(incoming[0].text, 60)}”`);
const title = [...link.shared.map((s) => `Shared: ${s}`), ...link.mentions.map((m) => `${m.topic.title}: ${m.text}`)].join('\n');
return h('li', {}, h('button', { type: 'button', title, onclick: () => navigate(other, { reveal: true }) },
swatch(other),
h('span', {}, other.title),
h('span', { class: 'why' }, reasons.join(' · '))));
});
return h('ul', { class: 'related' }, items);
}
function passes(r) {
const f = state.filters;
return (!f.types.size || f.types.has(r.type)) && (!f.beginner || r.beginner) && (!f.gem || r.gem);
}
function resetFilters(rerender = true) {
state.filters = { types: new Set(), beginner: false, gem: false };
if (rerender) rerenderPanel();
}
function filterBar(entries) {
const f = state.filters;
const present = Object.keys(TYPES).filter((type) => entries.some((r) => r.type === type));
const button = (label, pressed, onclick) => h('button', { type: 'button', class: 'filter', 'aria-pressed': String(pressed), onclick }, label);
const active = f.types.size || f.beginner || f.gem;
const hasBeginner = entries.some((r) => r.beginner);
const hasGem = entries.some((r) => r.gem);
if (present.length < 2 && !hasBeginner && !hasGem && !active) return null;
return h('div', { class: 'filters', role: 'group', 'aria-label': 'Filter resources' },
present.length > 1 ? present.map((type) => button(`${TYPES[type].icon} ${TYPES[type].label}`, f.types.has(type), () => {
if (f.types.has(type)) f.types.delete(type);
else f.types.add(type);
rerenderPanel();
})) : null,
hasBeginner ? button('👶 Beginner', f.beginner, () => { f.beginner = !f.beginner; rerenderPanel(); }) : null,
hasGem ? button('💎 Essential', f.gem, () => { f.gem = !f.gem; rerenderPanel(); }) : null,
active ? h('button', { type: 'button', class: 'filter reset', onclick: () => resetFilters() }, 'Clear filters') : null);
}
function resourceItem(r) {
const type = TYPES[r.type];
return h('li', { class: 'res', 'data-url': r.url },
h('span', { class: 'res-icon', title: type.label, 'aria-label': type.label, role: 'img' }, type.icon),
h('div', { class: 'res-body' },
h('a', { class: 'res-title', href: r.url, target: '_blank', rel: 'noopener noreferrer' }, r.title),
h('div', { class: 'res-meta' },
h('span', {}, r.domain),
r.beginner ? h('span', { class: 'badge beginner' }, 'Beginner') : null,
r.gem ? h('span', { class: 'badge gem' }, 'Essential') : null),
r.note ? h('p', { class: 'res-note' }, r.note) : null,
r.sharedWith.length ? h('div', { class: 'res-also' }, 'Also listed in ', r.sharedWith.flatMap((t, i) => [
i ? ', ' : null,
h('button', { type: 'button', class: 'link-btn', onclick: () => navigate(t, { reveal: true }) }, t.title),
])) : null));
}
function resources(node) {
const hasSub = node.children.length > 0 && node.total > node.resources.length;
const include = hasSub && (state.includeSub ?? node.resources.length === 0);
const groups = include
? [...descendants(node)].filter((n) => n.resources.length).map((n) => ({ node: n, list: n.resources }))
: [{ node, list: node.resources }];
const all = groups.flatMap((g) => g.list);
const shown = all.filter(passes).length;
const sec = h('section', { class: 'panel-section' },
h('div', { class: 'res-head' },
h('h3', {}, 'Resources'),
h('span', { class: 'count' }, shown === all.length ? plural(all.length, 'item') : `${shown} of ${all.length}`)));
if (hasSub) {
const box = h('input', { type: 'checkbox', checked: include });
box.addEventListener('change', () => {
state.includeSub = box.checked;
rerenderPanel();
});
sec.append(h('label', { class: 'toggle' }, box, `Include subtopics (${plural(node.total, 'resource')})`));
}
if (!all.length) {
sec.append(h('p', { class: 'empty' }, 'This topic only groups its subtopics. Pick one above.'));
return sec;
}
const filters = filterBar(all);
if (filters) sec.append(filters);
if (!shown) {
sec.append(h('div', { class: 'empty' }, 'No resources match these filters. ',
h('button', { type: 'button', class: 'link-btn', onclick: () => resetFilters() }, 'Clear filters')));
return sec;
}
for (const group of groups) {
const list = group.list.filter(passes);
if (!list.length) continue;
if (include) {
sec.append(h('div', { class: 'res-group' },
h('button', { type: 'button', onclick: () => navigate(group.node, { reveal: true }) }, group.node.title)));
}
sec.append(h('ul', { class: 'res-list' }, list.map(resourceItem)));
}
return sec;
}
function areaSummary() {
const boxes = state.plan ? state.plan.boxes.filter((b) => !b.header) : [];
const count = (test) => boxes.filter(test).length;
const tile = (areas, n, label) => h('div', { class: 'stat' }, areaDots(areas), h('b', {}, n), h('span', {}, label));
return h('div', { class: 'stats areas' },
tile(['SOFTWARE'], count((b) => b.areas.length === 1 && b.areas[0] === 'SOFTWARE'), 'Software only'),
tile(['SOFTWARE', 'HARDWARE'], count((b) => b.areas.includes('SOFTWARE') && b.areas.includes('HARDWARE') && !b.areas.includes('SOFT SKILLS')), 'Software ∩ Hardware'),
tile(['HARDWARE'], count((b) => b.areas.length === 1 && b.areas[0] === 'HARDWARE'), 'Hardware only'));
}
function overview(root) {
const { stats, groups } = state.data;
return [
section(null, description(root.description)),
section(null, h('div', { class: 'stats' },
h('div', { class: 'stat' }, h('b', {}, stats.topics), h('span', {}, 'topics')),
h('div', { class: 'stat' }, h('b', {}, stats.resources), h('span', {}, 'resources')),
h('div', { class: 'stat' }, h('b', {}, stats.links), h('span', {}, 'connections')))),
state.plan ? section('Diagram topics by area', areaSummary()) : null,
...groups.map((g) => section(g.title, topicChips(g.topics))),
section('How to explore', h('ul', { class: 'howto' },
h('li', {}, 'Map follows the roadmap diagram: topics sit in Software, Hardware or both, and are colored by importance.'),
h('li', {}, 'Use the Areas chips to highlight a single area or only the Software ∩ Hardware cross-section.'),
h('li', {}, 'Hover or select a topic to see lines to the topics it shares resources with (solid) or is mentioned by (dotted).'),
h('li', {}, 'Outline lists every topic of the README; use it to browse topics that are not on the map.'),
h('li', {}, 'Press ', h('kbd', {}, '/'), ' to search topics and resources.'))),
section(null, h('p', { class: 'res-note' }, 'Built from the ',
h('a', { href: `${REPO_URL}#readme`, target: '_blank', rel: 'noopener noreferrer' }, 'roadmap README'),
' and the map definition in the repository.')),
section('Credits', credits()),
].filter(Boolean);
}
function credits() {
const external = (href, ...children) => h('a', { href, target: '_blank', rel: 'noopener noreferrer' }, ...children);
const build = state.build;
return h('div', { class: 'credits' },
h('img', { class: 'credits-logo', src: 'assets/logo.svg', alt: '', width: 48, height: 48 }),
h('div', { class: 'credits-body' },
h('p', {}, 'Creator & maintainer: ', external(CREDITS.authorUrl, h('strong', {}, CREDITS.author))),
build ? h('p', {}, 'Revision ', h('strong', {}, build.version), ` · last update ${build.date}`) : null,
h('p', {}, 'Licensed under the ', external(CREDITS.licenseUrl, CREDITS.licenseName), '.'),
external(CREDITS.licenseUrl, h('img', { class: 'credits-badge', src: 'assets/cc-by-sa.svg', alt: CREDITS.license, width: 88, height: 31 }))));
}
function renderPanel(node) {
const root = state.data.root;
const panel = els.panel;
panel.replaceChildren();
panel.append(h('header', { class: 'panel-head' },
h('button', { type: 'button', class: 'icon-btn sheet-close', 'aria-label': 'Close details', onclick: () => openSheet(false) }, '✕'),
crumbs(node),
h('h2', { class: 'panel-title' },
node.icon ? h('span', { class: 'title-icon', 'aria-hidden': 'true' }, node.icon) : node === root ? null : swatch(node),
node === root ? 'Embedded Engineering Roadmap' : node.title),
node === root ? null : h('div', { class: 'meta' },
node.areas && node.areas.length ? h('span', { class: 'pill area-pill' }, areaDots(node.areas), areaLabel(node.areas)) : null,
node.importance ? h('span', { class: `pill imp-pill ${node.importance}` }, IMPORTANCE[node.importance]) : null,
h('span', { class: 'pill' }, plural(node.total, 'resource')),
node.children.length ? h('span', { class: 'pill' }, plural(node.children.length, 'subtopic')) : null,
node.links.length ? h('span', { class: 'pill' }, `${node.links.length} connected`) : null)));
if (node === root) {
panel.append(...overview(root));
return;
}
if (node.description.length) panel.append(section('About', description(node.description)));
if (node.children.length) panel.append(section('Subtopics', topicChips(node.children)));
if (node.links.length) panel.append(section('Connected topics', related(node)));
panel.append(resources(node));
}
function rerenderPanel() {
const top = els.panel.scrollTop;
renderPanel(state.selected);
els.panel.scrollTop = top;
}
function revealResource(resource) {
const item = [...els.panel.querySelectorAll('.res')].find((li) => li.dataset.url === resource.url);
if (!item) return;
item.scrollIntoView({ block: 'center', behavior: reducedMotion.matches ? 'auto' : 'smooth' });
item.classList.remove('flash');
void item.offsetWidth;
item.classList.add('flash');
}
function openSheet(open) {
els.panel.classList.toggle('open', open);
if (open) els.panel.focus({ preventScroll: true });
}
// ---------- outline view ----------
function buildOutline() {
const { groups, stats } = state.data;
const item = (node) => {
const button = h('button', {
type: 'button',
class: 'tree-btn',
'data-id': node.id,
onclick: (e) => {
e.preventDefault();
navigate(node, { reveal: true });
},
}, swatch(node), h('span', { class: 'tree-title' }, node.title),
node.areas && node.areas.length ? areaDots(node.areas) : null,
h('span', { class: 'tree-count' }, node.total));
if (!node.children.length) return h('li', {}, button);
const details = h('details', {}, h('summary', {}, button), h('ul', { class: 'tree' }, node.children.map(item)));
return h('li', {}, details);
};
els.outline.replaceChildren(
h('h2', {}, 'All topics'),
h('p', { class: 'res-note' }, `${stats.topics} topics and ${stats.resources} resources. Select a topic to see its details; use the arrows to expand.`),
...groups.map((g) => h('section', { class: 'outline-group' }, h('h3', {}, g.title), h('ul', { class: 'tree' }, g.topics.map(item)))),
);
}
function syncOutline() {
for (const btn of els.outline.querySelectorAll('.tree-btn[aria-current]')) btn.removeAttribute('aria-current');
const btn = state.selected && els.outline.querySelector(`.tree-btn[data-id="${CSS.escape(state.selected.id)}"]`);
if (!btn) return;
btn.setAttribute('aria-current', 'true');
for (let el = btn.parentElement; el && el !== els.outline; el = el.parentElement) {
if (el.tagName === 'DETAILS') el.open = true;
}
}
const VIEWS = ['map', 'list'];
function setView(view) {
if (!VIEWS.includes(view) || (view === 'map' && !state.mapView)) view = state.mapView ? 'map' : 'list';
els.diagram.hidden = view !== 'map';
els.outline.hidden = view !== 'list';
for (const tab of els.tabs) {
tab.setAttribute('aria-selected', String(tab.dataset.view === view));
if (tab.dataset.view === 'map') tab.hidden = !state.mapView;
}
store('roadmap-view', view);
if (view === 'map') state.mapView.show();
if (view === 'list') {
syncOutline();
const current = els.outline.querySelector('.tree-btn[aria-current]');
if (current) current.scrollIntoView({ block: 'center' });
}
}
// ---------- search ----------
function buildIndex() {
state.index = {
topics: state.data.topics.map((node) => ({
node,
title: normalize(node.title),
text: normalize([node.title, ...node.path].join(' ')),
})),
resources: state.data.topics.flatMap((node) => node.resources.map((r) => ({
node,
r,
text: normalize(`${r.title} ${r.domain}`),
}))),
};
}
function runSearch(query) {
const tokens = normalize(query).split(/\s+/).filter(Boolean);
if (!tokens.length) return [];
const match = (entry) => tokens.every((t) => entry.text.includes(t));
const topics = state.index.topics
.filter(match)
.map((e) => ({ ...e, score: e.title.startsWith(tokens[0]) ? 0 : e.title.includes(tokens[0]) ? 1 : 2 }))
.sort((a, b) => a.score - b.score || a.node.depth - b.node.depth)
.slice(0, 6)
.map((e) => ({ kind: 'topic', node: e.node }));
const seen = new Set();
const resourceHits = state.index.resources
.filter((e) => match(e) && !seen.has(e.r.url) && seen.add(e.r.url))
.slice(0, 8)
.map((e) => ({ kind: 'resource', node: e.node, r: e.r }));
return [...topics, ...resourceHits];
}
function renderResults() {
const box = els.results;
const query = els.search.value.trim();
box.replaceChildren();
if (!query) {
closeResults();
return;
}
const results = state.results;
if (!results.length) {
box.append(h('div', { class: 'result-empty' }, `No topics or resources match “${query}”.`));
}
let lastKind = '';
results.forEach((res, i) => {
if (res.kind !== lastKind) {
box.append(h('div', { class: 'result-group', role: 'presentation' }, res.kind === 'topic' ? 'Topics' : 'Resources'));
lastKind = res.kind;
}
const isTopic = res.kind === 'topic';
const option = h('div', {
id: `result-${i}`,
class: 'result',
role: 'option',
'aria-selected': String(i === state.active),
},
isTopic ? swatch(res.node) : h('span', { 'aria-hidden': 'true' }, TYPES[res.r.type].icon),
h('div', { class: 'result-main' },
h('div', { class: 'result-title' }, isTopic ? res.node.title : res.r.title, isTopic && res.node.areas && res.node.areas.length ? areaDots(res.node.areas) : null),
h('div', { class: 'result-sub' }, isTopic
? (res.node.path.length ? res.node.path.join(' › ') : plural(res.node.total, 'resource'))
: `${res.r.domain} · ${res.node.title}`)));
option.addEventListener('pointerdown', (e) => {
e.preventDefault();
choose(i);
});
box.append(option);
});
box.hidden = false;
els.search.setAttribute('aria-expanded', 'true');
els.search.setAttribute('aria-activedescendant', state.active >= 0 ? `result-${state.active}` : '');
}
function closeResults() {
els.results.hidden = true;
els.search.setAttribute('aria-expanded', 'false');
els.search.removeAttribute('aria-activedescendant');
state.active = -1;
}
function choose(index) {
const res = state.results[index];
if (!res) return;
closeResults();
els.search.value = '';
els.search.blur();
if (res.kind === 'topic') navigate(res.node, { reveal: true });
else navigate(res.node, { resource: res.r, reveal: true });
}
function wireSearch() {
els.search.addEventListener('input', () => {
state.results = runSearch(els.search.value);
state.active = state.results.length ? 0 : -1;
renderResults();
});
els.search.addEventListener('keydown', (e) => {
const count = state.results.length;
if (e.key === 'ArrowDown' && count) {
e.preventDefault();
state.active = (state.active + 1) % count;
renderResults();
document.getElementById(`result-${state.active}`)?.scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp' && count) {
e.preventDefault();
state.active = (state.active - 1 + count) % count;
renderResults();
document.getElementById(`result-${state.active}`)?.scrollIntoView({ block: 'nearest' });
} else if (e.key === 'Enter') {
e.preventDefault();
choose(state.active >= 0 ? state.active : 0);
} else if (e.key === 'Escape') {
e.stopPropagation();
if (els.search.value) {
els.search.value = '';
closeResults();
} else {
els.search.blur();
}
}
});
els.search.addEventListener('focus', () => {
if (els.search.value.trim()) renderResults();
});
els.search.addEventListener('blur', () => window.setTimeout(closeResults, 120));
}
// ---------- theme, loading, startup ----------
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
if (!state.data) return;
buildOutline();
syncOutline();
rerenderPanel();
}
async function loadMarkdown() {
for (const source of SOURCES) {
try {
const response = await fetch(source, { cache: 'no-cache' });
if (!response.ok) continue;
const text = await response.text();
if (text.includes('# Embedded Systems Engineering Roadmap')) return text;
} catch {
// Try the next source.
}
}
throw new Error('README.md could not be loaded');
}
async function loadMap() {
try {
const response = await fetch('map.json', { cache: 'no-cache' });
return response.ok ? await response.json() : null;
} catch {
return null;
}
}
// Revision and date, written by CI next to the deployed site; absent in local previews.
async function loadBuild() {
try {
const response = await fetch('build.json', { cache: 'no-cache' });
return response.ok ? await response.json() : null;
} catch {
return null;
}
}
function isTyping(el) {
return el instanceof HTMLElement && (el.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName));
}
function wireGlobal() {
els.theme.addEventListener('click', () => {
const next = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light';
store('roadmap-theme', next);
applyTheme(next);
});
for (const tab of els.tabs) tab.addEventListener('click', () => setView(tab.dataset.view));
document.getElementById('print').addEventListener('click', () => window.print());
// The PDF and PNG are rendered by CI next to the deployed site; local previews don't have them.
for (const link of document.querySelectorAll('[data-download]')) {
fetch(link.getAttribute('href'), { method: 'HEAD', cache: 'no-cache' })
.then((response) => { link.hidden = !response.ok; }, () => {});
}
// Printing always produces the diagram, whatever view is open.
let viewBeforePrint = null;
window.addEventListener('beforeprint', () => {
if (!state.mapView) return;
viewBeforePrint = store('roadmap-view');
setView('map');
state.mapView.prepareForPrint();
});
window.addEventListener('afterprint', () => {
if (state.mapView) state.mapView.afterPrint();
if (viewBeforePrint && viewBeforePrint !== 'map') setView(viewBeforePrint);
viewBeforePrint = null;
});
window.addEventListener('popstate', () => routeFromHash());
document.addEventListener('keydown', (e) => {
if (isTyping(e.target) || e.metaKey || e.ctrlKey || e.altKey) return;
if (e.key === '/') {
e.preventDefault();
els.search.focus();
} else if (e.key === 'Escape') {
if (els.panel.classList.contains('open')) openSheet(false);
else if (state.selected && state.selected.parent) navigate(state.selected.parent);
}
});
}
async function init() {
applyTheme(store('roadmap-theme') || (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'));
let markdown;
const diagramRequest = loadMap();
const buildRequest = loadBuild();
try {
markdown = await loadMarkdown();
} catch {
els.status.replaceChildren('The roadmap could not be loaded. ',
h('a', { href: `${REPO_URL}#readme` }, 'Read it on GitHub instead'), '.');
return;
}
state.data = parseRoadmap(markdown);
assignHues(state.data);
const diagram = await diagramRequest;
state.build = await buildRequest;
if (diagram) {
state.plan = linkDiagram(diagram, state.data);
state.mapView = createMapView({
root: els.diagram,
plan: state.plan,
data: state.data,
build: state.build,
onSelect: (topic) => navigate(topic, { reveal: true }),
});
}
buildOutline();
buildIndex();
wireSearch();
wireGlobal();
els.status.hidden = true;
routeFromHash();
setView(store('roadmap-view') || 'map');
}
init();
+199
View File
@@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="120"
height="42"
id="svg2759"
sodipodi:version="0.32"
inkscape:version="0.45+devel"
version="1.0"
sodipodi:docname="by-sa.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape">
<defs
id="defs2761" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#8b8b8b"
borderopacity="1"
gridtolerance="10000"
guidetolerance="10"
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="179"
inkscape:cy="89.569904"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="120px"
height="42px"
inkscape:showpageshadow="false"
inkscape:window-width="1198"
inkscape:window-height="624"
inkscape:window-x="488"
inkscape:window-y="401" />
<metadata
id="metadata2764">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
transform="matrix(0.9937807,0,0,0.9936694,-177.69409,-74.436409)"
id="g287"
inkscape:export-filename="/mnt/hgfs/Bov/Documents/Work/2007/cc/identity/srr buttons/big/by-sa.png"
inkscape:export-xdpi="300.23013"
inkscape:export-ydpi="300.23013">
<path
id="path3817_2_"
nodetypes="ccccccc"
d="M 182.23532,75.39014 L 296.29928,75.59326 C 297.89303,75.59326 299.31686,75.35644 299.31686,78.77344 L 299.17721,116.34033 L 179.3569,116.34033 L 179.3569,78.63379 C 179.3569,76.94922 179.51999,75.39014 182.23532,75.39014 z"
style="fill:#aab2ab" />
<g
id="g5908_2_"
transform="matrix(0.872921,0,0,0.872921,50.12536,143.2144)">
<path
id="path5906_2_"
cx="296.35416"
ry="22.939548"
cy="264.3577"
type="arc"
rx="22.939548"
d="M 187.20944,-55.6792 C 187.21502,-46.99896 180.18158,-39.95825 171.50134,-39.95212 C 162.82113,-39.94708 155.77929,-46.97998 155.77426,-55.66016 C 155.77426,-55.66687 155.77426,-55.67249 155.77426,-55.6792 C 155.76922,-64.36054 162.80209,-71.40125 171.48233,-71.40631 C 180.16367,-71.41193 187.20441,-64.37842 187.20944,-55.69824 C 187.20944,-55.69263 187.20944,-55.68591 187.20944,-55.6792 z"
style="fill:#ffffff" />
<g
id="g5706_2_"
transform="translate(-289.6157,99.0653)">
<path
id="path5708_2_"
d="M 473.88455,-167.54724 C 477.36996,-164.06128 479.11294,-159.79333 479.11294,-154.74451 C 479.11294,-149.69513 477.40014,-145.47303 473.9746,-142.07715 C 470.33929,-138.50055 466.04281,-136.71283 461.08513,-136.71283 C 456.18736,-136.71283 451.96526,-138.48544 448.42003,-142.03238 C 444.87419,-145.57819 443.10158,-149.81537 443.10158,-154.74451 C 443.10158,-159.6731 444.87419,-163.94049 448.42003,-167.54724 C 451.87523,-171.03375 456.09728,-172.77618 461.08513,-172.77618 C 466.13342,-172.77618 470.39914,-171.03375 473.88455,-167.54724 z M 450.76657,-165.20239 C 447.81982,-162.22601 446.34701,-158.7395 446.34701,-154.74005 C 446.34701,-150.7417 447.80529,-147.28485 450.72125,-144.36938 C 453.63778,-141.45288 457.10974,-139.99462 461.1383,-139.99462 C 465.16683,-139.99462 468.66848,-141.46743 471.64486,-144.41363 C 474.47076,-147.14947 475.88427,-150.59069 475.88427,-154.74005 C 475.88427,-158.85809 474.44781,-162.35297 471.57659,-165.22479 C 468.70595,-168.09546 465.22671,-169.53131 461.1383,-169.53131 C 457.04993,-169.53131 453.59192,-168.08813 450.76657,-165.20239 z M 458.52106,-156.49927 C 458.07074,-157.4809 457.39673,-157.9715 456.49781,-157.9715 C 454.90867,-157.9715 454.11439,-156.90198 454.11439,-154.763 C 454.11439,-152.62341 454.90867,-151.55389 456.49781,-151.55389 C 457.54719,-151.55389 458.29676,-152.07519 458.74647,-153.11901 L 460.94923,-151.94598 C 459.8993,-150.0805 458.32417,-149.14697 456.22374,-149.14697 C 454.60384,-149.14697 453.30611,-149.64367 452.33168,-150.63653 C 451.35561,-151.62994 450.86894,-152.99926 450.86894,-154.7445 C 450.86894,-156.46008 451.37123,-157.82159 452.37642,-158.83013 C 453.38161,-159.83806 454.63347,-160.34264 456.13423,-160.34264 C 458.35435,-160.34264 459.94407,-159.46776 460.90504,-157.71978 L 458.52106,-156.49927 z M 468.8844,-156.49927 C 468.43353,-157.4809 467.77292,-157.9715 466.90201,-157.9715 C 465.28095,-157.9715 464.46988,-156.90198 464.46988,-154.763 C 464.46988,-152.62341 465.28095,-151.55389 466.90201,-151.55389 C 467.95304,-151.55389 468.68918,-152.07519 469.10925,-153.11901 L 471.36126,-151.94598 C 470.31301,-150.0805 468.74007,-149.14697 466.64358,-149.14697 C 465.02587,-149.14697 463.73095,-149.64367 462.75711,-150.63653 C 461.78494,-151.62994 461.29773,-152.99926 461.29773,-154.7445 C 461.29773,-156.46008 461.79221,-157.82159 462.78061,-158.83013 C 463.76843,-159.83806 465.02588,-160.34264 466.55408,-160.34264 C 468.77027,-160.34264 470.35776,-159.46776 471.3154,-157.71978 L 468.8844,-156.49927 z" />
</g>
</g>
<path
d="M 297.29639,74.91064 L 181.06688,74.91064 C 179.8203,74.91064 178.80614,75.92529 178.80614,77.17187 L 178.80614,116.66748 C 178.80614,116.94922 179.03466,117.17822 179.31639,117.17822 L 299.04639,117.17822 C 299.32813,117.17822 299.55713,116.94922 299.55713,116.66748 L 299.55713,77.17188 C 299.55713,75.92529 298.54297,74.91064 297.29639,74.91064 z M 181.06688,75.93213 L 297.29639,75.93213 C 297.97998,75.93213 298.53565,76.48828 298.53565,77.17188 C 298.53565,77.17188 298.53565,93.09131 298.53565,104.59034 L 215.4619,104.59034 C 212.41698,110.09571 206.55077,113.83399 199.81835,113.83399 C 193.083,113.83399 187.21825,110.09913 184.1748,104.59034 L 179.82666,104.59034 C 179.82666,93.09132 179.82666,77.17188 179.82666,77.17188 C 179.82664,76.48828 180.38329,75.93213 181.06688,75.93213 z"
id="path294" />
<g
enable-background="new "
id="g296">
<path
d="M 265.60986,112.8833 C 265.68994,113.03906 265.79736,113.16504 265.93115,113.26172 C 266.06494,113.35791 266.22119,113.42969 266.40088,113.47608 C 266.58154,113.52296 266.76807,113.54639 266.96045,113.54639 C 267.09033,113.54639 267.22998,113.53565 267.3794,113.51368 C 267.52784,113.4922 267.66749,113.44972 267.79835,113.3877 C 267.92823,113.32569 268.03761,113.23975 268.12355,113.13086 C 268.21144,113.02197 268.25441,112.88379 268.25441,112.71533 C 268.25441,112.53515 268.19679,112.38916 268.08156,112.27685 C 267.9673,112.16455 267.81594,112.07177 267.62941,111.99658 C 267.44386,111.92236 267.23195,111.85693 266.9966,111.80078 C 266.76027,111.74463 266.52101,111.68262 266.27883,111.61377 C 266.02981,111.55176 265.78762,111.47559 265.55129,111.38525 C 265.31594,111.29541 265.10402,111.17822 264.9175,111.03515 C 264.73098,110.89208 264.58059,110.71337 264.46535,110.49853 C 264.35109,110.28369 264.29347,110.02392 264.29347,109.71923 C 264.29347,109.37646 264.36671,109.07958 264.51222,108.82763 C 264.6587,108.57568 264.85011,108.36572 265.08644,108.19726 C 265.32179,108.02929 265.58937,107.90478 265.8882,107.82372 C 266.18605,107.74315 266.48488,107.70263 266.78273,107.70263 C 267.13136,107.70263 267.46535,107.74169 267.78566,107.81982 C 268.105,107.89746 268.39015,108.02392 268.6382,108.19824 C 268.88722,108.37256 269.08449,108.59521 269.23097,108.86621 C 269.37648,109.13721 269.44972,109.46582 269.44972,109.85156 L 268.02784,109.85156 C 268.01514,109.65234 267.97315,109.4873 267.90284,109.35693 C 267.83155,109.22607 267.73682,109.12353 267.61964,109.04834 C 267.50148,108.97412 267.36671,108.9209 267.21534,108.89014 C 267.063,108.85889 266.89796,108.84326 266.71827,108.84326 C 266.60108,108.84326 266.48292,108.85596 266.36573,108.88037 C 266.24757,108.90576 266.14112,108.94922 266.04542,109.01123 C 265.94874,109.07373 265.86964,109.15137 265.80812,109.24463 C 265.7466,109.33838 265.71535,109.45654 265.71535,109.59961 C 265.71535,109.73047 265.73976,109.83643 265.78957,109.91699 C 265.83937,109.99804 265.93801,110.07275 266.08352,110.14111 C 266.22903,110.20947 266.43118,110.27832 266.68899,110.34668 C 266.9468,110.41504 267.28372,110.50244 267.70071,110.60791 C 267.82473,110.63281 267.99661,110.67822 268.21731,110.74365 C 268.43801,110.80908 268.65676,110.91308 268.87454,111.05615 C 269.09231,111.1997 269.27981,111.39111 269.43899,111.63037 C 269.59719,111.87012 269.67629,112.17676 269.67629,112.55029 C 269.67629,112.85547 269.61672,113.13867 269.49856,113.3999 C 269.3804,113.66162 269.20461,113.8872 268.97122,114.07666 C 268.73782,114.26709 268.44876,114.41455 268.10403,114.52051 C 267.75833,114.62647 267.35794,114.6792 266.90481,114.6792 C 266.53762,114.6792 266.18118,114.63379 265.83547,114.54346 C 265.49074,114.45313 265.18508,114.31104 264.92043,114.11768 C 264.65676,113.92432 264.4468,113.67774 264.29055,113.37891 C 264.13528,113.07959 264.06106,112.7251 264.06692,112.31397 L 265.4888,112.31397 C 265.48877,112.53809 265.52881,112.72803 265.60986,112.8833 z"
id="path298"
style="fill:#ffffff" />
<path
d="M 273.8667,107.8667 L 276.35986,114.53076 L 274.8374,114.53076 L 274.33349,113.04638 L 271.84033,113.04638 L 271.31787,114.53076 L 269.84326,114.53076 L 272.36377,107.8667 L 273.8667,107.8667 z M 273.95068,111.95264 L 273.11084,109.50928 L 273.09229,109.50928 L 272.22315,111.95264 L 273.95068,111.95264 z"
id="path300"
style="fill:#ffffff" />
</g>
<g
enable-background="new "
id="g302">
<path
d="M 239.17821,107.8667 C 239.49559,107.8667 239.78563,107.89502 240.04735,107.95068 C 240.30907,108.00683 240.53368,108.09863 240.72118,108.22607 C 240.9077,108.35351 241.05321,108.52295 241.15575,108.73437 C 241.25829,108.94579 241.31005,109.20703 241.31005,109.51806 C 241.31005,109.854 241.23388,110.13329 241.08056,110.35742 C 240.92822,110.58154 240.70165,110.76465 240.40283,110.90771 C 240.81494,111.02587 241.12256,111.23291 241.32568,111.5288 C 241.5288,111.82469 241.63037,112.18114 241.63037,112.59814 C 241.63037,112.93408 241.56494,113.22509 241.43408,113.47119 C 241.30322,113.7168 241.12646,113.91748 240.90576,114.07324 C 240.68408,114.229 240.43115,114.34424 240.14795,114.41845 C 239.86377,114.49365 239.57275,114.53075 239.27295,114.53075 L 236.03662,114.53075 L 236.03662,107.86669 L 239.17821,107.86669 L 239.17821,107.8667 z M 238.99071,110.56201 C 239.25243,110.56201 239.46727,110.5 239.63622,110.37597 C 239.80419,110.25146 239.88817,110.05029 239.88817,109.77099 C 239.88817,109.61572 239.85985,109.48828 239.80419,109.38915 C 239.74755,109.28954 239.67333,109.21239 239.57958,109.15624 C 239.48583,109.10058 239.37841,109.06151 239.25731,109.04003 C 239.13524,109.01806 239.00926,109.00732 238.8784,109.00732 L 237.50535,109.00732 L 237.50535,110.56201 L 238.99071,110.56201 z M 239.07664,113.39014 C 239.22019,113.39014 239.35691,113.37647 239.48777,113.34815 C 239.61863,113.32032 239.73484,113.27344 239.83445,113.2085 C 239.93406,113.14307 240.01316,113.0542 240.07273,112.94239 C 240.1323,112.83058 240.1616,112.68751 240.1616,112.51319 C 240.1616,112.17139 240.06492,111.92725 239.87156,111.78126 C 239.6782,111.63527 239.42234,111.56202 239.10496,111.56202 L 237.50535,111.56202 L 237.50535,113.39014 L 239.07664,113.39014 z"
id="path304"
style="fill:#ffffff" />
<path
d="M 241.88914,107.8667 L 243.53269,107.8667 L 245.09324,110.49854 L 246.64402,107.8667 L 248.27781,107.8667 L 245.80418,111.97315 L 245.80418,114.53077 L 244.33543,114.53077 L 244.33543,111.93604 L 241.88914,107.8667 z"
id="path306"
style="fill:#ffffff" />
</g>
<g
id="g6316_1_"
transform="matrix(0.624995,0,0,0.624995,391.2294,176.9332)">
<path
id="path6318_1_"
cx="475.97119"
ry="29.209877"
cy="252.08646"
type="arc"
rx="29.209877"
d="M -175.0083,-139.1153 C -175.00204,-129.7035 -182.62555,-122.06751 -192.03812,-122.06049 C -201.44913,-122.05341 -209.08512,-129.67774 -209.09293,-139.09028 C -209.09293,-139.09809 -209.09293,-139.10749 -209.09293,-139.1153 C -209.09919,-148.52784 -201.47413,-156.1623 -192.06311,-156.17011 C -182.65054,-156.17713 -175.01456,-148.55207 -175.0083,-139.14026 C -175.0083,-139.13092 -175.0083,-139.1239 -175.0083,-139.1153 z"
style="fill:#ffffff" />
<g
id="g6320_1_"
transform="translate(-23.9521,-89.72962)">
<path
id="path6322_1_"
d="M -168.2204,-68.05536 C -173.39234,-68.05536 -177.76892,-66.25067 -181.35175,-62.64203 C -185.02836,-58.90759 -186.86588,-54.48883 -186.86588,-49.38568 C -186.86588,-44.28253 -185.02836,-39.89416 -181.35175,-36.22308 C -177.67673,-32.55114 -173.29859,-30.71521 -168.2204,-30.71521 C -163.07974,-30.71521 -158.62503,-32.56677 -154.85312,-36.26996 C -151.30307,-39.78558 -149.52652,-44.15827 -149.52652,-49.38568 C -149.52652,-54.6123 -151.33432,-59.03265 -154.94843,-62.64203 C -158.5625,-66.25067 -162.98599,-68.05536 -168.2204,-68.05536 z M -168.17352,-64.69519 C -163.936,-64.69519 -160.33752,-63.20221 -157.37655,-60.21466 C -154.38748,-57.25836 -152.89214,-53.64899 -152.89214,-49.38568 C -152.89214,-45.09186 -154.35466,-41.52856 -157.28438,-38.69653 C -160.36876,-35.64727 -163.99849,-34.12304 -168.17351,-34.12304 C -172.34856,-34.12304 -175.94701,-35.63244 -178.96892,-38.64965 C -181.9908,-41.66918 -183.50176,-45.24657 -183.50176,-49.38567 C -183.50176,-53.52398 -181.97518,-57.13414 -178.92205,-60.21465 C -175.9939,-63.20221 -172.41107,-64.69519 -168.17352,-64.69519 z" />
<path
id="path6324_1_"
d="M -176.49548,-52.02087 C -175.75171,-56.71856 -172.44387,-59.22949 -168.30008,-59.22949 C -162.33911,-59.22949 -158.70783,-54.90448 -158.70783,-49.1372 C -158.70783,-43.50982 -162.57194,-39.13793 -168.39383,-39.13793 C -172.39856,-39.13793 -175.98297,-41.60277 -176.63611,-46.43877 L -171.93292,-46.43877 C -171.7923,-43.92778 -170.1626,-43.04418 -167.83447,-43.04418 C -165.1813,-43.04418 -163.4563,-45.50908 -163.4563,-49.27709 C -163.4563,-53.22942 -164.94693,-55.32244 -167.74228,-55.32244 C -169.79074,-55.32244 -171.55948,-54.57787 -171.93292,-52.02087 L -170.56418,-52.02789 L -174.26734,-48.32629 L -177.96894,-52.02789 L -176.49548,-52.02087 z" />
</g>
</g>
<g
id="g313">
<circle
cx="242.56226"
cy="90.224609"
r="10.8064"
id="circle315"
sodipodi:cx="242.56226"
sodipodi:cy="90.224609"
sodipodi:rx="10.8064"
sodipodi:ry="10.8064"
style="fill:#ffffff" />
<g
id="g317">
<path
d="M 245.68994,87.09766 C 245.68994,86.68116 245.35205,86.34424 244.93603,86.34424 L 240.16357,86.34424 C 239.74755,86.34424 239.40966,86.68115 239.40966,87.09766 L 239.40966,91.87061 L 240.74071,91.87061 L 240.74071,97.52295 L 244.3579,97.52295 L 244.3579,91.87061 L 245.68993,91.87061 L 245.68993,87.09766 L 245.68994,87.09766 z"
id="path319" />
<circle
cx="242.5498"
cy="84.083008"
r="1.63232"
id="circle321"
sodipodi:cx="242.5498"
sodipodi:cy="84.083008"
sodipodi:rx="1.63232"
sodipodi:ry="1.63232" />
</g>
<path
clip-rule="evenodd"
d="M 242.53467,78.31836 C 239.30322,78.31836 236.56641,79.4458 234.32715,81.70215 C 232.0293,84.03516 230.88086,86.79736 230.88086,89.98633 C 230.88086,93.1753 232.0293,95.91846 234.32715,98.21338 C 236.625,100.50781 239.36133,101.65527 242.53467,101.65527 C 245.74756,101.65527 248.53272,100.49853 250.88819,98.18359 C 253.10889,95.98681 254.21827,93.2539 254.21827,89.98632 C 254.21827,86.71874 253.08936,83.95751 250.83057,81.70214 C 248.57178,79.4458 245.80615,78.31836 242.53467,78.31836 z M 242.56396,80.41797 C 245.2124,80.41797 247.46142,81.35156 249.31103,83.21875 C 251.18115,85.06592 252.11572,87.32227 252.11572,89.98633 C 252.11572,92.66992 251.20068,94.89746 249.36963,96.66699 C 247.4419,98.57275 245.17334,99.52539 242.56397,99.52539 C 239.9546,99.52539 237.70557,98.58252 235.81739,96.6958 C 233.92774,94.80957 232.98389,92.57324 232.98389,89.98633 C 232.98389,87.3999 233.93799,85.14404 235.84619,83.21875 C 237.67676,81.35156 239.9165,80.41797 242.56396,80.41797 z"
id="path323"
style="fill-rule:evenodd" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" width="300" height="300">
<rect width="300" height="300" fill="#afd074"/>
<g fill="#000">
<rect x="135" y="65" width="30" height="30"/><rect x="170" y="65" width="30" height="30"/><rect x="205" y="65" width="30" height="30"/>
<rect x="135" y="100" width="30" height="30"/><rect x="205" y="100" width="30" height="30"/>
<rect x="65" y="135" width="30" height="30"/><rect x="100" y="135" width="30" height="30"/><rect x="135" y="135" width="30" height="30"/><rect x="170" y="135" width="30" height="30"/><rect x="205" y="135" width="30" height="30"/>
<rect x="65" y="170" width="30" height="30"/><rect x="135" y="170" width="30" height="30"/>
<rect x="65" y="205" width="30" height="30"/><rect x="100" y="205" width="30" height="30"/><rect x="135" y="205" width="30" height="30"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 879 B

+144
View File
@@ -0,0 +1,144 @@
// Links the topics drawn on the map (map.json) to README topics, so every topic knows
// which areas (Software, Hardware, Soft skills) it sits in and how important the map marks it.
// Map labels whose README heading is worded differently.
const ALIASES = {
'ADC / DAC': ['ADC', 'DAC'],
'Buildroot / Yocto': ['Buildroot', 'Yocto'],
'TDD & Unit Testing': ['Test Driven Development (TDD)', 'Unit Testing'],
'Threading / Parallelism': ['Multithreading & Parallel Processing'],
'Device Drivers': ['Linux Device Drivers'],
'Real-Time OS': ['Real-Time Operating Systems'],
'Interfaces & Protocols': ['Interfaces, Protocols & Communication Technologies'],
Basic: ['Basic Protocols'],
'High-Speed': ['High-Speed Protocols'],
Wireless: ['Wireless Protocols'],
Industrial: ['Industrial Protocols'],
Automotive: ['Automotive Protocols'],
Network: ['Network Protocols / Socket Programming'],
'TCP/IP': ['Network Protocols / Socket Programming'],
UDP: ['Network Protocols / Socket Programming'],
Cellular: ['Cellular Communication'],
MQTT: ['CoAP & MQTT'],
CoAP: ['CoAP & MQTT'],
'LTE-M / 5G': ['LTE-M & NB-IoT'],
'NB-IoT': ['LTE-M & NB-IoT'],
'Basic Math & Calculus': ['Basic Calculus'],
'SDLC Models': ['Software Development Life Cycle (SDLC) Models'],
'Version Control': ['Version Control Systems'],
AUTOSAR: ['AUTOSAR Architecture'],
// Drawn on the map but without their own README section: open the protocol family instead.
Profinet: ['Industrial Protocols'],
LIN: ['Automotive Protocols'],
MOST: ['Automotive Protocols'],
FlexRay: ['Automotive Protocols'],
UWB: ['Wireless Protocols'],
};
export const AREA_ORDER = ['SOFTWARE', 'HARDWARE', 'SOFT SKILLS'];
export const IMPORTANCE_LEVELS = ['required', 'recommended', 'possible'];
const RANK = { required: 3, recommended: 2, possible: 1 };
const key = (text) => text.normalize('NFKC').toLowerCase().replace(/\s+/g, ' ').trim();
function topicIndex(topics) {
const index = new Map();
const add = (k, topic) => {
if (k && !index.has(k)) index.set(k, topic);
};
const byDepth = [...topics].sort((a, b) => a.depth - b.depth);
for (const t of byDepth) add(key(t.title), t);
for (const t of byDepth) {
const base = t.title.replace(/\s*\([^)]*\)\s*/g, ' ').trim();
add(key(base), t);
const inner = t.title.match(/\(([^)]+)\)/);
if (inner && /^[A-Z0-9-]{2,6}$/.test(inner[1])) add(key(inner[1]), t);
add(key(base.split(' / ')[0]), t);
}
return index;
}
export function linkDiagram(map, data) {
const index = topicIndex(data.topics);
const softSkills = data.topics.find((t) => t.depth === 1 && key(t.title) === 'soft skills');
const resolve = (text, areas = []) => {
if (ALIASES[text]) return ALIASES[text].map((title) => index.get(key(title))).filter(Boolean);
const topic = index.get(key(text));
if (topic) return [topic];
if (softSkills && areas.includes('SOFT SKILLS')) return [softSkills];
return [];
};
// A cluster's areas are those of the band it sits in; a group's are the areas all its clusters share.
const clusterAreas = new Map();
const clusters = {};
const boxes = [];
for (const band of map.bands) {
for (const [cluster, spec] of Object.entries(band.clusters)) {
clusterAreas.set(cluster, band.areas);
clusters[cluster] = { header: spec.header, caption: spec.caption, per: spec.per };
if (spec.header) boxes.push({ text: spec.header, importance: null, areas: band.areas, cluster, header: true });
for (const [text, importance] of spec.topics) boxes.push({ text, importance, areas: band.areas, cluster, header: false });
}
}
boxes.forEach((b, i) => Object.assign(b, { id: `box-${i}`, topics: resolve(b.text, b.areas) }));
const regions = Object.entries(map.groups).map(([name, clusters]) => {
const areas = AREA_ORDER.filter((a) => clusters.every((c) => (clusterAreas.get(c) || []).includes(a)));
return { name, clusters, areas, topics: resolve(name) };
});
const boxOf = new Map();
for (const t of data.topics) {
t.areas = [];
t.importance = null;
t.boxes = [];
}
for (const box of boxes) {
for (const t of box.topics) {
t.boxes.push(box);
if (!boxOf.has(t)) boxOf.set(t, box);
for (const area of box.areas) if (!t.areas.includes(area)) t.areas.push(area);
if (box.importance && (!t.importance || RANK[box.importance] > RANK[t.importance])) t.importance = box.importance;
}
}
for (const region of regions) {
for (const t of region.topics) {
for (const area of region.areas) if (!t.areas.includes(area)) t.areas.push(area);
}
}
// Subtopics that are not drawn in the diagram inherit the areas of their closest drawn ancestor.
for (const t of data.topics) {
if (!t.areas.length) for (let p = t.parent; p && !t.areas.length; p = p.parent) t.areas = [...(p.areas || [])];
t.areas.sort((a, b) => AREA_ORDER.indexOf(a) - AREA_ORDER.indexOf(b));
}
// The box that stands for a topic on the map: its own, or the closest ancestor's.
const boxFor = (topic) => {
for (let t = topic; t; t = t.parent) if (boxOf.has(t)) return boxOf.get(t);
return null;
};
const pairs = new Map();
for (const link of data.links) {
const a = boxFor(link.source);
const b = boxFor(link.target);
if (!a || !b || a === b) continue;
const [p, q] = a.id < b.id ? [a, b] : [b, a];
const id = `${p.id}|${q.id}`;
if (!pairs.has(id)) pairs.set(id, { a: p, b: q, weight: 0, shared: 0, links: [] });
const pair = pairs.get(id);
pair.weight += link.weight;
pair.shared += link.shared.length;
pair.links.push(link);
}
return {
regions,
clusters,
boxes,
boxFor,
connections: [...pairs.values()],
unmatched: boxes.filter((b) => !b.header && !b.topics.length).map((b) => b.text),
invalid: boxes.filter((b) => !b.header && !IMPORTANCE_LEVELS.includes(b.importance)).map((b) => `${b.text}: ${b.importance}`),
};
}
+57
View File
@@ -0,0 +1,57 @@
// DOM helpers shared by the views.
export const SVG_NS = 'http://www.w3.org/2000/svg';
export const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
export const CREDITS = {
author: 'Meysam Parvizi',
authorUrl: 'https://github.com/m3y54m',
repo: 'github.com/m3y54m/Embedded-Engineering-Roadmap',
license: 'CC BY-SA 4.0',
licenseName: 'Creative Commons Attribution-ShareAlike 4.0 International License',
licenseUrl: 'https://creativecommons.org/licenses/by-sa/4.0/',
};
export const GITHUB_MARK = 'M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z';
export function h(tag, props = {}, ...children) {
const el = document.createElement(tag);
for (const [key, value] of Object.entries(props)) {
if (value == null || value === false) continue;
if (key === 'class') el.className = value;
else if (key.startsWith('on')) el.addEventListener(key.slice(2), value);
else el.setAttribute(key, value === true ? '' : value);
}
for (const child of children.flat(Infinity)) {
if (child == null || child === false || child === '') continue;
el.append(child instanceof Node ? child : document.createTextNode(String(child)));
}
return el;
}
export function svg(tag, attrs = {}, parent = null) {
const el = document.createElementNS(SVG_NS, tag);
for (const [key, value] of Object.entries(attrs)) if (value != null) el.setAttribute(key, value);
if (parent) parent.append(el);
return el;
}
export const AREAS = {
SOFTWARE: { label: 'Software', cls: 'sw' },
HARDWARE: { label: 'Hardware', cls: 'hw' },
'SOFT SKILLS': { label: 'Soft skills', cls: 'ss' },
};
export const IMPORTANCE = {
required: 'Required',
recommended: 'Recommended',
possible: 'Possibility',
};
export const areaClass = (area) => (AREAS[area] ? AREAS[area].cls : 'xx');
export const areaLabel = (areas) => areas.map((a) => (AREAS[a] ? AREAS[a].label : a)).join(' ∩ ');
export function areaDots(areas) {
return h('span', { class: 'area-dots', 'aria-hidden': 'true' }, areas.map((a) => h('i', { class: `dot ${areaClass(a)}` })));
}
+93
View File
@@ -0,0 +1,93 @@
Copyright 2019 The Sofia Sans Project Authors (https://github.com/lettersoup/Sofia-Sans)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+70
View File
@@ -0,0 +1,70 @@
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' data:; connect-src 'self' https://raw.githubusercontent.com; base-uri 'none'; form-action 'none'; object-src 'none'">
<title>Embedded Engineering Roadmap · Explorer</title>
<meta name="description" content="Interactive map of the Embedded Systems Engineering Roadmap: explore topics, how they connect, and their learning resources.">
<link rel="icon" href="assets/logo.svg" type="image/svg+xml">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="map.css">
<script type="module" src="app.js"></script>
</head>
<body>
<div class="app">
<header class="topbar">
<a class="brand" href="#/" aria-label="Roadmap overview">
<img class="brand-mark" src="assets/logo.svg" alt="" width="36" height="36">
<span class="brand-text">
<strong>Embedded Engineering Roadmap</strong>
<span>Interactive explorer</span>
</span>
</a>
<div class="search" role="search">
<svg class="search-icon" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/><path d="m20 20-4.2-4.2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input id="search" type="search" placeholder="Search topics and resources" autocomplete="off" spellcheck="false"
role="combobox" aria-label="Search topics and resources" aria-expanded="false" aria-controls="search-results" aria-autocomplete="list">
<kbd aria-hidden="true">/</kbd>
<div id="search-results" class="search-results" role="listbox" aria-label="Search results" hidden></div>
</div>
<div class="actions">
<div class="segmented" role="tablist" aria-label="View">
<button type="button" role="tab" data-view="map" aria-selected="true" aria-controls="diagram">Map</button>
<button type="button" role="tab" data-view="list" aria-selected="false" aria-controls="outline">Outline</button>
</div>
<button type="button" id="print" class="icon-btn" aria-label="Print or save the roadmap as PDF" title="Print or save as PDF (A4)">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M7 9V3h10v6M7 17H5a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2M7 14h10v7H7z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>
</button>
<a class="icon-btn text-btn" href="Embedded-Engineering-Roadmap.pdf" download data-download hidden title="Download the roadmap as PDF (A4)">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M12 4v11m0 0-4.5-4.5M12 15l4.5-4.5M5 19h14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg>PDF
</a>
<a class="icon-btn text-btn" href="Embedded-Engineering-Roadmap.png" download data-download hidden title="Download the roadmap as PNG image">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M12 4v11m0 0-4.5-4.5M12 15l4.5-4.5M5 19h14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg>PNG
</a>
<button type="button" id="theme" class="icon-btn" aria-label="Switch color theme" title="Switch color theme">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M12 3a9 9 0 1 0 9 9A7 7 0 0 1 12 3Z" fill="currentColor"/></svg>
</button>
<a class="icon-btn" href="https://github.com/m3y54m/Embedded-Engineering-Roadmap" target="_blank" rel="noopener noreferrer" aria-label="Repository on GitHub" title="Repository on GitHub">
<svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg>
</a>
</div>
</header>
<main class="layout">
<section class="stage" aria-label="Roadmap">
<div id="diagram" class="map-view" role="tabpanel" hidden></div>
<div id="outline" class="outline" role="tabpanel" hidden></div>
<div id="status" class="status" role="status">
<span class="spinner" aria-hidden="true"></span>
<span>Loading roadmap…</span>
</div>
</section>
<aside id="panel" class="panel" aria-label="Topic details" tabindex="-1"></aside>
</main>
</div>
</body>
</html>
+167
View File
@@ -0,0 +1,167 @@
// Print-friendly layout (A4 portrait proportions) for the roadmap map.
// Topics come from map.json; this file only decides where each cluster goes. Three horizontal bands
// make the areas explicit: Software only (top), Software ∩ Hardware (middle), Hardware only
// (bottom). Soft skills are not technical, so they get their own area below the others. The
// Real-Time OS cluster sits where the Operating Systems and Microcontrollers groups overlap.
// Minimum distance between any two boxes, texts or lines.
export const SPACE = 10;
const COL = 170;
const GUTTER = 26;
const LEFT = 86;
const TOP = 168;
const BOX_H = 26;
const BOX_H2 = 42;
const HEAD_H = 20;
const HEAD = HEAD_H + SPACE;
const GAP = 2 * SPACE;
const FOOTER_H = 56;
// Rough width of one character of box text, used to decide when a label wraps to two lines.
const CHAR_W = 6.3;
const TEXT_PAD = 8;
export const PAGE_WIDTH = LEFT * 2 + COL * 5 + GUTTER * 4;
const colX = (i) => LEFT + i * (COL + GUTTER);
const spanW = (n) => n * COL + (n - 1) * GUTTER;
// Cluster ids from map.json that this layout knows where to place.
export const PLACED = [
'languages', 'fundamentals', 'debugging', 'sdlc', 'vcs', 'build', 'testing', 'specialized', 'osBase', 'linux',
'rtos', 'mcu', 'bridge', 'basic', 'wireless', 'highSpeed', 'industrial', 'cellular', 'network', 'automotive',
'electronics', 'equipment', 'prototyping', 'fpga', 'soft',
];
function place(cluster, x, y, w) {
const items = [];
let cy = y;
if (cluster.headerBox || cluster.caption) {
items.push({
kind: cluster.headerBox ? 'header' : 'caption',
box: cluster.headerBox || null,
text: cluster.headerBox ? cluster.headerBox.text : cluster.caption,
x, y: cy, w, h: HEAD_H,
});
cy += HEAD;
}
const per = cluster.per || 1;
const bw = (w - (per - 1) * SPACE) / per;
for (let i = 0; i < cluster.boxes.length; i += per) {
const row = cluster.boxes.slice(i, i + per);
// Every box in a row gets the same height so the row stays aligned.
const h = row.some((b) => b.text.length * CHAR_W + 2 * TEXT_PAD > bw && b.text.includes(' ')) ? BOX_H2 : BOX_H;
row.forEach((box, j) => items.push({ kind: 'box', box, text: box.text, x: x + j * (bw + SPACE), y: cy, w: bw, h }));
cy += h + SPACE;
}
return { items, bottom: cy - SPACE };
}
export function buildLayout(plan) {
const clusters = {};
for (const id of PLACED) {
const spec = plan.clusters[id] || {};
const own = plan.boxes.filter((b) => b.cluster === id);
clusters[id] = { ...spec, headerBox: own.find((b) => b.header) || null, boxes: own.filter((b) => !b.header) };
}
// Topics in clusters this layout cannot place yet; the tests report them.
const missing = plan.boxes.filter((b) => !PLACED.includes(b.cluster)).map((b) => `${b.cluster}: ${b.text}`);
const items = [];
const add = (result) => {
items.push(...result.items);
return result.bottom;
};
const stack = (ids, x, y, w) => ids.reduce((top, id, i) => add(place(clusters[id], x, i ? top + GAP : top, w)), y);
const c = [0, 1, 2, 3, 4].map(colX);
const wide = spanW(2);
// Group tags are 22 high and centered on the group's top border.
const TAG = 11;
const belowTag = (y) => y + TAG + SPACE;
// Software only.
const languagesBottom = add(place(clusters.languages, c[0], TOP, COL));
const fundamentalsBottom = add(place(clusters.fundamentals, c[1], TOP, COL));
const col3 = stack(['debugging', 'sdlc', 'vcs'], c[2], TOP, COL);
const col4 = stack(['build', 'testing'], c[3], TOP, COL);
const col5 = add(place(clusters.specialized, c[4], TOP, COL));
// OS outline is 2 spaces outside its content, Microcontrollers 1 space, and the Interfaces
// outline keeps a full space from both inside the gutter between them.
const leftW = wide + GUTTER - 4 * SPACE;
const osTop = Math.max(languagesBottom, fundamentalsBottom) + GAP + TAG;
let osY = add(place(clusters.osBase, c[0], belowTag(osTop), leftW));
osY = add(place(clusters.linux, c[0], osY + GAP, leftW));
// Software ∩ Hardware.
const crossTop = Math.max(osY, col3, col4, col5) + GAP;
const groupTop = crossTop + GAP;
const rtosBottom = add(place(clusters.rtos, c[0], belowTag(groupTop), leftW));
const os = { x: c[0] - 2 * SPACE, y: osTop, w: leftW + 4 * SPACE, h: rtosBottom + SPACE - osTop };
const mcuBottom = add(place(clusters.mcu, c[0], os.y + os.h + SPACE, leftW));
const mcu = { x: c[0] - SPACE, y: groupTop, w: leftW + 2 * SPACE, h: mcuBottom + SPACE - groupTop };
const subW = (wide - 2 * SPACE) / 3;
const sub = [0, 1, 2].map((i) => c[2] + i * (subW + SPACE));
const ifContent = belowTag(groupTop);
const ifBottom = Math.max(
stack(['basic', 'wireless'], sub[0], ifContent, subW),
stack(['highSpeed', 'industrial', 'cellular'], sub[1], ifContent, subW),
stack(['network', 'automotive'], sub[2], ifContent, subW),
);
const interfaces = { x: c[2] - SPACE, y: groupTop, w: wide + 2 * SPACE, h: ifBottom + SPACE - groupTop };
const bridgeBottom = add(place(clusters.bridge, c[4], groupTop, COL));
const crossBottom = Math.max(mcu.y + mcu.h, interfaces.y + interfaces.h, bridgeBottom) + GAP;
// Hardware only.
const hwTop = crossBottom + GAP;
const hwBottom = Math.max(
add(place(clusters.electronics, c[0], hwTop, wide)),
add(place(clusters.equipment, c[2], hwTop, COL)),
add(place(clusters.prototyping, c[3], hwTop, COL)),
add(place(clusters.fpga, c[4], hwTop + HEAD, COL)),
);
const areaX = 24;
const areaW = PAGE_WIDTH - 2 * areaX;
const swTop = TOP - 40;
const hwEnd = hwBottom + GAP;
// Soft skills: a separate area under the technical ones, tall enough for its margin label.
const softTop = hwEnd + GAP;
const softRows = Math.ceil(clusters.soft.boxes.length / clusters.soft.per);
const softH = Math.max(132, softRows * (BOX_H2 + SPACE) - SPACE + 2 * GAP);
const soft = { x: areaX, y: softTop, w: areaW, h: softH };
const softContentH = place(clusters.soft, 0, 0, spanW(5)).bottom;
add(place(clusters.soft, c[0], softTop + (softH - softContentH) / 2, spanW(5)));
const softEnd = softTop + softH;
const regionByName = new Map(plan.regions.map((r) => [r.name, r]));
const groupTopics = (name) => (regionByName.get(name) || { topics: [] }).topics;
const footer = { x: areaX, y: softEnd + GAP, w: areaW, h: FOOTER_H };
return {
width: PAGE_WIDTH,
height: footer.y + footer.h + 16,
titleY: 58,
footer,
areas: {
SOFTWARE: { x: areaX, y: swTop, w: areaW, h: crossBottom - swTop },
HARDWARE: { x: areaX, y: crossTop, w: areaW, h: hwEnd - crossTop },
'SOFT SKILLS': soft,
},
cross: { x: areaX, y: crossTop, w: areaW, h: crossBottom - crossTop },
bands: [
{ label: 'SOFTWARE', cls: 'sw', y0: swTop, y1: crossTop },
{ label: 'SOFTWARE ∩ HARDWARE', cls: 'both', y0: crossTop, y1: crossBottom },
{ label: 'HARDWARE', cls: 'hw', y0: crossBottom, y1: hwEnd },
{ label: 'SOFT SKILLS', cls: 'ss', y0: softTop, y1: softEnd },
],
groups: [
{ name: 'Operating Systems', cls: 'g-os', rect: os, topics: groupTopics('Operating Systems') },
{ name: 'Microcontrollers', cls: 'g-mcu', rect: mcu, topics: groupTopics('Microcontrollers') },
{ name: 'Interfaces & Protocols', cls: 'g-ifc', rect: interfaces, topics: groupTopics('Interfaces & Protocols') },
],
items,
missing,
};
}
+516
View File
@@ -0,0 +1,516 @@
/* Map view: the roadmap diagram. */
.map-view {
position: absolute;
inset: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
.map-top {
position: relative;
z-index: 2;
display: grid;
gap: 8px;
padding: 12px 16px 10px;
border-bottom: 1px solid var(--border);
background: var(--surface);
backdrop-filter: blur(12px);
}
.start-strip,
.map-toolbar {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 6px;
overflow-x: auto;
scrollbar-width: thin;
padding-bottom: 2px;
}
.start-strip > *,
.map-toolbar > * {
flex: none;
}
.map-toolbar-label {
min-width: 74px;
color: var(--muted);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.start-chip,
.map-chip {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 30px;
padding: 4px 12px;
border: 1px solid var(--border-strong);
border-radius: 999px;
background: var(--surface-2);
color: var(--text);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.start-chip:hover,
.map-chip:hover {
border-color: var(--accent);
background: var(--chip-hover);
}
.map-chip > span {
display: inline-flex;
align-items: center;
gap: 7px;
}
.map-chip[aria-pressed="true"] {
border-color: var(--accent);
background: var(--accent);
color: var(--bg);
}
.map-chip.clear {
border-style: dashed;
background: transparent;
}
.map-toolbar-sep {
width: 1px;
height: 22px;
margin: 0 4px;
background: var(--border-strong);
}
.map-summary {
margin-left: 4px;
color: var(--muted);
font-size: 12.5px;
}
.dot.both {
background: linear-gradient(90deg, var(--sw) 50%, var(--hw) 50%);
}
.imp-swatch {
display: inline-block;
width: 16px;
height: 11px;
border: 1px solid var(--box-edge);
border-radius: 3px;
}
.imp-swatch.required { background: var(--imp-required); }
.imp-swatch.recommended { background: var(--imp-recommended); }
.imp-swatch.possible { background: var(--imp-possible); border-color: var(--border-strong); }
.map-canvas {
position: relative;
min-height: 0;
}
.diagram {
display: block;
width: 100%;
height: 100%;
font-family: var(--font-display);
touch-action: none;
user-select: none;
cursor: grab;
}
.diagram.panning {
cursor: grabbing;
}
.page-bg {
fill: transparent;
}
.page-title {
fill: var(--text);
font-size: 34px;
font-weight: 900;
letter-spacing: -0.01em;
}
.legend-text {
fill: var(--text);
font-size: 13px;
font-weight: 600;
}
.legend-note {
fill: var(--muted);
font-size: 12px;
font-style: italic;
}
.legend-imp { stroke: var(--box-edge); stroke-width: 1; }
.legend-imp.required { fill: var(--imp-required); }
.legend-imp.recommended { fill: var(--imp-recommended); }
.legend-imp.possible { fill: var(--imp-possible); stroke: var(--border-strong); }
.legend-area { fill: none; stroke-width: 2.5; }
.legend-area.sw { stroke: var(--sw); }
.legend-area.hw { stroke: var(--hw); }
.legend-area.ss { stroke: var(--ss); }
.legend-hatch { fill: url(#cross-hatch); stroke: var(--border-strong); }
.area-fill {
stroke: none;
fill-opacity: var(--area-tint);
}
.area-fill.sw { fill: var(--sw); }
.area-fill.hw { fill: var(--hw); }
.area-fill.ss { fill: var(--ss); fill-opacity: calc(var(--area-tint) * 1.6); }
.area-line {
fill: none;
stroke-width: 3;
}
.area-line.sw { stroke: var(--sw); }
.area-line.hw { stroke: var(--hw); }
.area-line.ss { stroke: var(--ss); stroke-width: 2.5; }
.overlap-fill {
fill: url(#cross-hatch);
}
.hatch-a,
.hatch-b {
stroke-width: 2.4;
stroke-opacity: 0.2;
}
.hatch-a { stroke: var(--sw); }
.hatch-b { stroke: var(--hw); }
.band-label {
font-size: 12px;
font-weight: 800;
letter-spacing: 0.22em;
}
.band-label.sw { fill: var(--sw-text); }
.band-label.hw { fill: var(--hw-text); }
.band-label.both { fill: var(--text); }
.band-label.ss { fill: var(--ss-text); }
.group-line {
fill: none;
stroke-width: 2;
}
.group-line.g-os { stroke: var(--g-os); }
.group-line.g-mcu { stroke: var(--g-mcu); }
.group-line.g-ifc { stroke: var(--g-ifc); }
.tag-bg {
fill: var(--surface-solid);
stroke-width: 2;
}
.tag-text {
font-size: 14px;
font-weight: 800;
pointer-events: none;
}
.group-tag.g-os .tag-bg { stroke: var(--g-os); }
.group-tag.g-mcu .tag-bg { stroke: var(--g-mcu); }
.group-tag.g-ifc .tag-bg { stroke: var(--g-ifc); }
.group-tag.g-os .tag-text { fill: var(--g-os); }
.group-tag.g-mcu .tag-text { fill: var(--g-mcu); }
.group-tag.g-ifc .tag-text { fill: var(--g-ifc); }
.cluster-caption {
fill: var(--muted);
font-size: 11px;
font-weight: 800;
letter-spacing: 0.14em;
}
.box .box-bg {
stroke: var(--box-edge);
stroke-width: 1;
transition: filter 0.15s, stroke 0.15s;
}
.box.required .box-bg { fill: var(--imp-required); }
.box.recommended .box-bg { fill: var(--imp-recommended); }
.box.possible .box-bg { fill: var(--imp-possible); stroke: var(--border-strong); }
.box-text {
font-size: 14px;
font-weight: 600;
pointer-events: none;
}
.box.required .box-text { fill: var(--imp-required-text); }
.box.recommended .box-text { fill: var(--imp-recommended-text); }
.box.possible .box-text { fill: var(--imp-possible-text); }
.box .header-text {
fill: var(--text);
font-size: 15px;
font-weight: 800;
}
.target {
cursor: pointer;
outline: none;
}
.box.target:hover .box-bg,
.box.lit .box-bg {
filter: brightness(1.06);
stroke: var(--accent);
stroke-width: 2.2;
}
.box.header.target:hover .header-text,
.box.header.lit .header-text,
.group-tag.target:hover .tag-text {
text-decoration: underline;
}
.group-tag.target:hover .tag-bg {
fill: var(--chip-hover);
}
.box.selected .box-bg {
stroke: var(--text);
stroke-width: 3;
}
.box.header.selected .header-text {
fill: var(--accent);
}
.box.target:focus-visible .box-bg,
.group-tag.target:focus-visible .tag-bg {
stroke: var(--accent);
stroke-width: 3.5;
}
.box.header.target:focus-visible .header-text {
fill: var(--accent);
text-decoration: underline;
}
.layer-boxes .box {
transition: opacity 0.2s;
}
.diagram.filtering .box:not(.match, .header) {
opacity: 0.15;
}
.diagram.has-focus:not(.filtering) .box:not(.lit, .header) {
opacity: 0.35;
}
.link,
.link-all {
fill: none;
stroke: var(--accent);
stroke-linecap: round;
pointer-events: none;
}
.link.mention,
.link-all.mention {
stroke-dasharray: 5 5;
}
.link-all {
stroke-opacity: 0.4;
}
.layer-linksAll {
opacity: 0;
transition: opacity 0.25s;
}
.diagram.show-all .layer-linksAll {
opacity: 1;
}
.diagram.show-all.has-focus .layer-linksAll {
opacity: 0.2;
}
.link-end {
fill: var(--accent);
}
.credit-main {
fill: var(--text);
font-size: 14px;
font-weight: 700;
}
.credit-text {
fill: var(--muted);
font-size: 12.5px;
font-weight: 600;
}
.credit-strong {
fill: var(--text);
font-weight: 800;
}
.credit-icon {
fill: var(--text);
}
.page-credits a:hover text {
text-decoration: underline;
}
.zoom-controls {
position: absolute;
right: 16px;
bottom: 16px;
display: grid;
justify-items: center;
gap: 6px;
padding: 6px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
backdrop-filter: blur(10px);
}
.zoom-controls .icon-btn {
width: 34px;
height: 34px;
font-size: 18px;
font-weight: 700;
}
.zoom-level {
color: var(--muted);
font-size: 11px;
font-variant-numeric: tabular-nums;
}
@media (max-width: 960px) {
.map-top {
padding: 8px 10px;
}
.start-strip,
.map-toolbar {
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: none;
}
.start-chip,
.map-chip,
.map-toolbar-label,
.map-summary {
flex: none;
}
.zoom-controls {
bottom: 12px;
right: 12px;
}
}
/* Printing: the diagram alone on an A4 page, in print colors. */
@page {
size: A4 portrait;
margin: 8mm;
}
@media print {
html[data-theme] {
color-scheme: light;
--bg: #ffffff;
--text: #0f172a;
--muted: #475569;
--surface-solid: #ffffff;
--border-strong: rgba(15, 23, 42, 0.35);
--chip-hover: #ffffff;
--accent: #3b4fd8;
--sw: #2b9fbf;
--hw: #e8a400;
--ss: #12b85a;
--sw-text: #0b6a85;
--hw-text: #8a5b00;
--ss-text: #0b7a3b;
--g-os: #6d4fb0;
--g-mcu: #b2453c;
--g-ifc: #4b5567;
--area-tint: 0.06;
--imp-required: #fff04a;
--imp-required-text: #1d1a00;
--imp-recommended: #fbd7bb;
--imp-recommended-text: #2b1407;
--imp-possible: #f1f3f6;
--imp-possible-text: #1f2937;
--box-edge: rgba(15, 23, 42, 0.75);
}
html,
body {
height: auto;
overflow: visible;
background: #ffffff;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
.topbar,
.panel,
.map-top,
.zoom-controls,
.tooltip,
#outline,
.status {
display: none !important;
}
.app,
.layout,
.stage,
.map-view,
.map-canvas {
display: block !important;
position: static !important;
height: auto !important;
overflow: visible !important;
}
.diagram {
width: 100%;
height: auto;
max-height: 280mm;
}
.diagram .box,
.diagram .box-bg {
opacity: 1 !important;
filter: none !important;
}
.layer-links,
.layer-linksAll {
display: none;
}
}
+266
View File
@@ -0,0 +1,266 @@
{
"bands": [
{
"label": "SOFTWARE",
"areas": ["SOFTWARE"],
"clusters": {
"languages": {
"header": "Programming Languages",
"per": 2,
"topics": [
["C", "required"],
["C++", "recommended"],
["Python", "recommended"],
["Assembly", "possible"],
["Rust", "possible"]
]
},
"fundamentals": {
"header": "Programming Fundamentals",
"topics": [
["Algorithms & Data Structures", "required"],
["Design Patterns", "required"],
["State Machines", "required"],
["Memory Management", "required"]
]
},
"debugging": {
"header": "Debugging",
"topics": [
["JTAG / SWD", "required"],
["GDB", "required"],
["OpenOCD", "possible"]
]
},
"sdlc": {
"header": "SDLC Models",
"per": 2,
"topics": [
["Agile / SCRUM", "recommended"],
["V-Model", "recommended"]
]
},
"vcs": {
"header": "Version Control",
"per": 2,
"topics": [
["Git", "required"],
["SVN", "possible"]
]
},
"build": {
"header": "Build System",
"topics": [
["Compilers / GCC", "required"],
["Make / CMake", "required"],
["Bash Scripting", "recommended"],
["Docker", "possible"]
]
},
"testing": {
"header": "Testing",
"topics": [
["TDD & Unit Testing", "required"],
["CI/CD Pipelines", "recommended"],
["SIL / HIL Testing", "possible"],
["Standards & Certifications", "possible"]
]
},
"specialized": {
"caption": "Specializations",
"topics": [
["Embedded Security", "possible"],
["Embedded GUI", "possible"],
["IoT", "possible"],
["Edge AI", "possible"],
["AUTOSAR", "possible"]
]
},
"osBase": {
"topics": [
["Operating System Fundamentals", "recommended"]
]
},
"linux": {
"header": "Embedded Linux",
"per": 2,
"topics": [
["Linux Kernel", "recommended"],
["Device Drivers", "recommended"],
["U-Boot", "possible"],
["Buildroot / Yocto", "possible"],
["Threading / Parallelism", "possible"],
["IPC", "possible"],
["Qt Framework", "possible"]
]
}
}
},
{
"label": "SOFTWARE ∩ HARDWARE",
"areas": ["SOFTWARE", "HARDWARE"],
"clusters": {
"rtos": {
"header": "Real-Time OS",
"per": 2,
"topics": [
["RTOS Basics", "required"],
["FreeRTOS", "recommended"],
["Zephyr", "recommended"],
["QNX", "possible"],
["µC/OS", "possible"],
["RT-Thread", "possible"]
]
},
"mcu": {
"per": 2,
"topics": [
["GPIO", "required"],
["ADC / DAC", "required"],
["Timers / Counters", "required"],
["PWM", "required"],
["Watchdog", "required"],
["Interrupts", "required"],
["DMA", "required"],
["Clock Management", "required"],
["Power Management", "recommended"],
["Bootloader / DFU", "recommended"]
]
},
"bridge": {
"caption": "Hardware-facing software",
"topics": [
["Memory Technologies & File Systems", "recommended"],
["Hardware Simulation / Emulation", "recommended"],
["Sensors & Actuators", "possible"],
["Digital Signal Processing", "possible"],
["Control Theory", "possible"]
]
},
"basic": {
"header": "Basic",
"topics": [
["UART", "required"],
["I2C", "required"],
["SPI", "required"]
]
},
"wireless": {
"header": "Wireless",
"topics": [
["Bluetooth", "recommended"],
["Wi-Fi", "recommended"],
["LoRa", "possible"],
["Zigbee", "possible"],
["Thread", "possible"],
["Matter", "possible"],
["UWB", "possible"]
]
},
"highSpeed": {
"header": "High-Speed",
"topics": [
["Ethernet", "recommended"],
["USB", "recommended"],
["PCIe", "possible"]
]
},
"industrial": {
"header": "Industrial",
"topics": [
["Modbus", "recommended"],
["Profinet", "possible"],
["EtherCAT", "possible"]
]
},
"cellular": {
"header": "Cellular",
"topics": [
["GSM / LTE", "possible"],
["LTE-M / 5G", "possible"],
["NB-IoT", "possible"]
]
},
"network": {
"header": "Network",
"topics": [
["MQTT", "recommended"],
["CoAP", "possible"],
["TCP/IP", "possible"],
["UDP", "possible"]
]
},
"automotive": {
"header": "Automotive",
"topics": [
["CAN", "recommended"],
["LIN", "possible"],
["MOST", "possible"],
["FlexRay", "possible"]
]
}
}
},
{
"label": "HARDWARE",
"areas": ["HARDWARE"],
"clusters": {
"electronics": {
"header": "Electronics",
"per": 2,
"topics": [
["Basic Math & Calculus", "required"],
["Principles of Electric Circuits", "required"],
["Electronics Fundamentals", "required"],
["Digital Design", "required"],
["Computer Architecture", "required"]
]
},
"equipment": {
"header": "Using Test Equipment",
"topics": [
["Multimeter", "required"],
["Logic / Protocol Analyzer", "required"],
["Oscilloscope", "recommended"]
]
},
"prototyping": {
"header": "Prototyping Skills",
"topics": [
["Breadboarding", "recommended"],
["Hardware Design Basics", "recommended"],
["PCB Design / EMC", "recommended"],
["Soldering / Rework", "recommended"]
]
},
"fpga": {
"topics": [
["FPGA Development", "possible"]
]
}
}
},
{
"label": "SOFT SKILLS",
"areas": ["SOFT SKILLS"],
"clusters": {
"soft": {
"per": 3,
"topics": [
["Communication Skills", "required"],
["Problem-Solving Skills & Critical Thinking", "required"],
["Teamwork & Collaborative Abilities", "required"],
["Organizational & Time Management Skills", "required"],
["Being Self-Driven and Independent", "required"],
["Adaptability & Patience", "required"]
]
}
}
}
],
"groups": {
"Operating Systems": ["osBase", "linux", "rtos"],
"Microcontrollers": ["rtos", "mcu"],
"Interfaces & Protocols": ["basic", "wireless", "highSpeed", "industrial", "cellular", "network", "automotive"]
}
}
+572
View File
@@ -0,0 +1,572 @@
// Interactive roadmap diagram: Software / Hardware / Soft skills areas with their cross-sections,
// importance colors, filters and connections between topics. Laid out by layout.js to fit A4.
import { AREAS, CREDITS, GITHUB_MARK, IMPORTANCE, areaDots, areaLabel, h, plural, svg } from './dom.js';
import { SPACE, buildLayout } from './layout.js';
const MIN_ZOOM = 0.6;
const MAX_ZOOM = 6;
function intersect(a, b) {
const x = Math.max(a.x, b.x);
const y = Math.max(a.y, b.y);
const w = Math.min(a.x + a.w, b.x + b.w) - x;
const hh = Math.min(a.y + a.h, b.y + b.h) - y;
return w > 0 && hh > 0 ? { x, y, w, h: hh } : null;
}
const center = (r) => [r.x + r.w / 2, r.y + r.h / 2];
const rect = (r, attrs, parent) => svg('rect', { x: r.x, y: r.y, width: r.w, height: r.h, ...attrs }, parent);
function splitInTwo(text) {
const words = text.split(' ');
let best = [text];
let score = Infinity;
for (let i = 1; i < words.length; i += 1) {
const a = words.slice(0, i).join(' ');
const b = words.slice(i).join(' ');
if (Math.max(a.length, b.length) < score) {
score = Math.max(a.length, b.length);
best = [a, b];
}
}
return best;
}
export function createMapView({ root, plan, data, build, onSelect }) {
const layout = buildLayout(plan);
const state = { selected: null, hover: null, areas: new Set(), importance: new Set(), showAll: false, view: null, drag: null };
const boxEls = new Map();
const rectOf = new Map();
const legendParts = [];
// ---------- toolbar ----------
const chip = (label, cls, pressed, onclick, title) => h('button', {
type: 'button', class: `map-chip ${cls}`, 'aria-pressed': String(pressed), onclick, title,
}, label);
const toolbar = h('div', { class: 'map-toolbar', role: 'toolbar', 'aria-label': 'Highlight topics' });
const summary = h('span', { class: 'map-summary', role: 'status' });
function renderToolbar() {
const cross = state.areas.size === 2 && state.areas.has('SOFTWARE') && state.areas.has('HARDWARE');
toolbar.replaceChildren(...[
h('span', { class: 'map-toolbar-label' }, 'Highlight'),
Object.entries(AREAS).map(([area, info]) => chip(h('span', {}, h('i', { class: `dot ${info.cls}` }), info.label),
'', state.areas.has(area) && !cross, () => {
state.areas = state.areas.has(area) && !cross ? new Set() : new Set([area]);
refresh();
}, `Topics in ${info.label}`)),
chip(h('span', {}, h('i', { class: 'dot both' }), 'Software ∩ Hardware'), 'cross', cross, () => {
state.areas = cross ? new Set() : new Set(['SOFTWARE', 'HARDWARE']);
refresh();
}, 'Topics that belong to both Software and Hardware'),
h('span', { class: 'map-toolbar-sep', 'aria-hidden': 'true' }),
Object.entries(IMPORTANCE).map(([level, label]) => chip(h('span', {}, h('i', { class: `imp-swatch ${level}` }), label),
'', state.importance.has(level), () => toggle(state.importance, level))),
h('span', { class: 'map-toolbar-sep', 'aria-hidden': 'true' }),
chip('Show all connections', 'links', state.showAll, () => { state.showAll = !state.showAll; refresh(); },
'Lines between topics that share resources or mention each other'),
state.areas.size || state.importance.size ? chip('Clear', 'clear', false, () => {
state.areas.clear();
state.importance.clear();
refresh();
}) : null,
summary,
].flat().filter(Boolean));
}
function toggle(set, value) {
if (set.has(value)) set.delete(value);
else set.add(value);
refresh();
}
// README topics that the diagram does not cover.
const extra = data.root.children.filter((t) => !t.areas.length);
const strip = h('nav', { class: 'start-strip', 'aria-label': 'Getting started topics' },
h('span', { class: 'map-toolbar-label' }, 'Start here'),
extra.map((t) => h('button', { type: 'button', class: 'start-chip', onclick: () => onSelect(t) },
t.icon ? h('span', { 'aria-hidden': 'true' }, t.icon) : null, t.title)));
// ---------- svg ----------
const chart = svg('svg', {
class: 'diagram',
role: 'group',
'aria-label': 'Roadmap diagram. Tab moves between topics, Enter opens one.',
preserveAspectRatio: 'xMidYMid meet',
});
const defs = svg('defs', {}, chart);
const hatch = svg('pattern', { id: 'cross-hatch', width: 10, height: 10, patternUnits: 'userSpaceOnUse', patternTransform: 'rotate(45)' }, defs);
svg('line', { x1: 0, y1: 0, x2: 0, y2: 10, class: 'hatch-a' }, hatch);
svg('line', { x1: 5, y1: 0, x2: 5, y2: 10, class: 'hatch-b' }, hatch);
const layers = {};
for (const name of ['page', 'areas', 'overlap', 'outlines', 'groups', 'linksAll', 'links', 'boxes', 'labels']) {
layers[name] = svg('g', { class: `layer-${name}` }, chart);
}
rect({ x: 0, y: 0, w: layout.width, h: layout.height }, { class: 'page-bg' }, layers.page);
// Title and legend, so the diagram also stands on its own when printed.
const title = svg('text', { x: 24, y: layout.titleY, class: 'page-title' }, layers.labels);
title.textContent = 'Embedded Systems Engineering Roadmap';
const legend = svg('g', { class: 'page-legend' }, layers.labels);
const legendItems = [
...Object.entries(IMPORTANCE).map(([level, label]) => ({ kind: 'imp', cls: level, label })),
{ kind: 'area', cls: 'sw', label: 'Software' },
{ kind: 'area', cls: 'hw', label: 'Hardware' },
{ kind: 'hatch', cls: '', label: 'Software ∩ Hardware' },
{ kind: 'area', cls: 'ss', label: 'Soft skills' },
];
for (const item of legendItems) {
const y = layout.titleY + 22;
const cls = item.kind === 'imp' ? `legend-imp ${item.cls}` : item.kind === 'area' ? `legend-area ${item.cls}` : 'legend-hatch';
const key = rect({ x: 0, y, w: 22, h: 13 }, { rx: item.kind === 'imp' ? 3 : 4, class: cls }, legend);
const t = svg('text', { x: 0, y: y + 11, class: 'legend-text' }, legend);
t.textContent = item.label;
legendParts.push({ key, t, label: item.label });
}
function layoutLegend() {
let x = 26;
for (const part of legendParts) {
part.key.setAttribute('x', x);
part.t.setAttribute('x', x + 28);
x += 28 + ((part.t.getComputedTextLength && part.t.getComputedTextLength()) || part.label.length * 6.5) + 20;
}
}
const note = svg('text', { x: 24, y: layout.titleY + 54, class: 'legend-note' }, layers.labels);
note.textContent = 'Colors show the average importance of each skill for an embedded systems engineer.';
// Areas and their cross-sections.
const sw = layout.areas.SOFTWARE;
const hw = layout.areas.HARDWARE;
const ss = layout.areas['SOFT SKILLS'];
rect(sw, { rx: 28, class: 'area-fill sw' }, layers.areas);
rect(hw, { rx: 28, class: 'area-fill hw' }, layers.areas);
rect(ss, { rx: 28, class: 'area-fill ss' }, layers.areas);
rect(layout.cross, { class: 'overlap-fill' }, layers.overlap);
rect(sw, { rx: 28, class: 'area-line sw' }, layers.outlines);
rect(hw, { rx: 28, class: 'area-line hw' }, layers.outlines);
rect(ss, { rx: 28, class: 'area-line ss' }, layers.outlines);
// Band names run down the left margin; every band reads as one area or a cross-section.
for (const band of layout.bands) {
const cy = (band.y0 + band.y1) / 2;
const t = svg('text', {
x: 0, y: 0, class: `band-label ${band.cls}`, 'text-anchor': 'middle', 'dominant-baseline': 'central',
transform: `translate(${sw.x + 18} ${cy.toFixed(1)}) rotate(-90)`,
}, layers.labels);
t.textContent = band.label;
}
const tags = [];
function tag(text, cls, r, topic = null) {
const g = svg('g', { class: `tag ${cls}` }, layers.labels);
const bg = svg('rect', { class: 'tag-bg', height: 22, rx: 11 }, g);
const label = svg('text', { class: 'tag-text', 'dominant-baseline': 'central' }, g);
label.textContent = text;
const entry = { g, bg, label, text, r };
tags.push(entry);
if (topic) wireTarget(g, topic, null, `${text} group`);
return entry;
}
// Group labels all sit on the top-left of their own border.
for (const group of layout.groups) {
rect(group.rect, { rx: 16, class: `group-line ${group.cls}` }, layers.groups);
tag(group.name, `group-tag ${group.cls}`, group.rect, group.topics[0] || null);
}
function layoutTags() {
for (const t of tags) {
const width = (t.label.getComputedTextLength && t.label.getComputedTextLength()) || t.text.length * 7;
const x = t.r.x + 22;
const y = t.r.y - 11;
t.bg.setAttribute('x', x.toFixed(1));
t.bg.setAttribute('y', y.toFixed(1));
t.bg.setAttribute('width', (width + 22).toFixed(1));
t.label.setAttribute('x', (x + 11).toFixed(1));
t.label.setAttribute('y', (y + 11).toFixed(1));
}
}
// Credits: where the resources are, who maintains the roadmap, its license and revision.
const { x: fx, y: fy, w: fw } = layout.footer;
const fr = fx + fw;
const credits = svg('g', { class: 'page-credits' }, layers.labels);
const link = (href) => svg('a', { href, target: '_blank', rel: 'noopener noreferrer' }, credits);
const icon = svg('svg', { x: fx, y: fy + 2, width: 16, height: 16, viewBox: '0 0 16 16' }, credits);
svg('path', { d: GITHUB_MARK, class: 'credit-icon' }, icon);
const repo = svg('text', { x: fx + 24, y: fy + 15, class: 'credit-main' }, link(`https://${CREDITS.repo}`));
repo.textContent = `Learning resources for every topic: ${CREDITS.repo}`;
const creditLine = (y, label, value) => {
const t = svg('text', { x: fx, y, class: 'credit-text' }, credits);
t.append(label);
svg('tspan', { class: 'credit-strong' }, t).textContent = value;
};
creditLine(fy + 35, 'Creator & Maintainer: ', CREDITS.author);
if (build) creditLine(fy + 53, 'Revision: ', `${build.version} · Last update: ${build.date}`);
const logoSize = 52;
const badge = { w: 103, h: 36 };
svg('image', { href: 'assets/logo.svg', x: fr - logoSize, y: fy, width: logoSize, height: logoSize }, credits);
const badgeX = fr - logoSize - SPACE - badge.w;
svg('image', { href: 'assets/cc-by-sa.svg', x: badgeX, y: fy + (logoSize - badge.h) / 2, width: badge.w, height: badge.h }, link(CREDITS.licenseUrl));
const licenseText = svg('text', { x: badgeX - 14, y: fy + 22, 'text-anchor': 'end', class: 'credit-text' }, link(CREDITS.licenseUrl));
licenseText.textContent = 'This work is licensed under the Creative Commons';
const licenseText2 = svg('text', { x: badgeX - 14, y: fy + 40, 'text-anchor': 'end', class: 'credit-text' }, link(CREDITS.licenseUrl));
licenseText2.textContent = 'Attribution-ShareAlike 4.0 International License.';
// ---------- topics ----------
for (const item of layout.items) {
const box = item.box;
if (item.kind === 'caption') {
const t = svg('text', { x: item.x, y: item.y + 15, class: 'cluster-caption' }, layers.boxes);
t.textContent = item.text.toUpperCase();
continue;
}
const topic = box.topics[0] || null;
const g = svg('g', { class: `box ${item.kind === 'header' ? 'header' : box.importance}`, 'data-box': box.id }, layers.boxes);
rectOf.set(box, item);
if (item.kind === 'header') {
const t = svg('text', { x: item.x, y: item.y + 16, class: 'box-text header-text' }, g);
t.textContent = item.text;
boxEls.set(box, { g, text: t, width: item.w, header: true });
} else {
rect(item, { rx: 5, class: 'box-bg' }, g);
const lines = item.h >= 36 ? splitInTwo(item.text) : [item.text];
const t = svg('text', { x: item.x + item.w / 2, y: item.y + item.h / 2, 'text-anchor': 'middle', class: 'box-text' }, g);
lines.forEach((line, i) => {
const span = svg('tspan', { x: item.x + item.w / 2, dy: i === 0 ? `${0.36 - (lines.length - 1) * 0.55}em` : '1.1em' }, t);
span.textContent = line;
});
boxEls.set(box, { g, text: t, width: item.w - 10 });
}
for (const t of box.topics) {
if (!t.layoutBoxes) t.layoutBoxes = [];
t.layoutBoxes.push(box);
}
if (topic) wireTarget(g, topic, box);
}
function describe(topic, box) {
const parts = [box ? box.text : topic.title];
if (topic.areas.length) parts.push(areaLabel(topic.areas));
const importance = (box && box.importance) || topic.importance;
if (importance) parts.push(IMPORTANCE[importance]);
parts.push(plural(topic.total, 'resource'));
return parts.join(', ');
}
function wireTarget(el, topic, box = null, label = null) {
el.setAttribute('tabindex', '0');
el.setAttribute('role', 'button');
el.setAttribute('aria-label', label ? `${label}, ${plural(topic.total, 'resource')}` : describe(topic, box));
el.classList.add('target');
el.addEventListener('pointerenter', (e) => hover(topic, box, e));
el.addEventListener('pointermove', moveTip);
el.addEventListener('pointerleave', () => hover(null));
el.addEventListener('focus', () => hover(topic, box));
el.addEventListener('blur', () => hover(null));
el.addEventListener('click', () => {
if (state.drag && state.drag.moved) return;
onSelect(topic);
});
el.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onSelect(topic);
}
});
}
// ---------- tooltip ----------
const tip = h('div', { class: 'tooltip', hidden: true });
function hover(topic, box = null, e = null) {
state.hover = topic;
drawLinks();
if (!topic || !e) {
tip.hidden = true;
return;
}
const importance = (box && box.importance) || topic.importance;
tip.replaceChildren(...[
h('strong', {}, topic.title),
topic.areas.length ? h('div', { class: 'tip-areas' }, areaDots(topic.areas), areaLabel(topic.areas)) : null,
h('div', { class: 'tip-meta' }, [importance ? IMPORTANCE[importance] : null, plural(topic.total, 'resource'),
topic.links && topic.links.length ? `${topic.links.length} connected` : null].filter(Boolean).join(' · ')),
h('div', { class: 'tip-hint' }, 'Click for details and resources'),
].filter(Boolean));
tip.hidden = false;
moveTip(e);
}
function moveTip(e) {
if (tip.hidden) return;
const bounds = root.getBoundingClientRect();
let x = e.clientX - bounds.left + 16;
let y = e.clientY - bounds.top + 16;
if (x + tip.offsetWidth > bounds.width - 8) x = e.clientX - bounds.left - tip.offsetWidth - 16;
if (y + tip.offsetHeight > bounds.height - 8) y = e.clientY - bounds.top - tip.offsetHeight - 16;
tip.style.transform = `translate(${Math.max(8, x)}px, ${Math.max(8, y)}px)`;
}
// ---------- connections ----------
const connections = plan.connections.filter((c) => rectOf.has(c.a) && rectOf.has(c.b));
function curve(a, b) {
const [x1, y1] = center(rectOf.get(a));
const [x2, y2] = center(rectOf.get(b));
const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2;
const bend = 0.2;
return `M${x1.toFixed(1)} ${y1.toFixed(1)}Q${(mx - (y2 - y1) * bend).toFixed(1)} ${(my + (x2 - x1) * bend).toFixed(1)} ${x2.toFixed(1)} ${y2.toFixed(1)}`;
}
function drawConnection(connection, layer, cls) {
svg('path', {
d: curve(connection.a, connection.b),
class: `${cls} ${connection.shared ? 'shared' : 'mention'}`,
'stroke-width': (1.4 + Math.min(3, Math.sqrt(connection.weight) * 0.7)).toFixed(2),
}, layer);
if (cls !== 'link') return;
for (const box of [connection.a, connection.b]) {
const [cx, cy] = center(rectOf.get(box));
svg('circle', { cx: cx.toFixed(1), cy: cy.toFixed(1), r: 3, class: 'link-end' }, layer);
}
}
for (const connection of connections) drawConnection(connection, layers.linksAll, 'link-all');
const isWithin = (node, topic) => {
for (let n = node; n; n = n.parent) if (n === topic) return true;
return false;
};
function drawLinks() {
layers.links.replaceChildren();
const topic = state.hover || state.selected;
const lit = new Set();
if (topic) {
const own = new Set(plan.boxes.filter((b) => rectOf.has(b) && b.topics.some((t) => isWithin(t, topic))));
const fallback = plan.boxFor(topic);
if (fallback && rectOf.has(fallback)) own.add(fallback);
for (const b of own) lit.add(b);
for (const connection of connections) {
if (!(own.has(connection.a) || own.has(connection.b))) continue;
if (!connection.links.some((l) => isWithin(l.source, topic) || isWithin(l.target, topic))) continue;
drawConnection(connection, layers.links, 'link');
lit.add(connection.a);
lit.add(connection.b);
}
}
chart.classList.toggle('has-focus', Boolean(state.hover));
for (const [box, el] of boxEls) {
el.g.classList.toggle('lit', lit.has(box));
el.g.classList.toggle('selected', Boolean(state.selected) && box.topics.includes(state.selected));
}
}
// ---------- filters ----------
function matches(box) {
if (!rectOf.has(box) || boxEls.get(box).header) return false;
for (const area of state.areas) if (!box.areas.includes(area)) return false;
if (state.areas.size === 1 && box.areas.filter((a) => a !== 'SOFT SKILLS').length > 1 && !state.areas.has('SOFT SKILLS')) {
// A single area means "only this area", so the cross-section stands apart.
return false;
}
return !state.importance.size || state.importance.has(box.importance);
}
function refresh() {
const filtering = state.areas.size > 0 || state.importance.size > 0;
chart.classList.toggle('filtering', filtering);
chart.classList.toggle('show-all', state.showAll);
let count = 0;
for (const [box, el] of boxEls) {
const match = filtering && matches(box);
if (match) count += 1;
el.g.classList.toggle('match', match);
}
summary.textContent = filtering ? `${plural(count, 'topic')}` : '';
renderToolbar();
}
// ---------- pan and zoom ----------
const page = { w: layout.width, h: layout.height };
const MAX_SCALE = 1.5;
let fitMode = 'width';
function canvasSize() {
const box = chart.getBoundingClientRect();
return box.width > 0 && box.height > 0 ? { w: box.width, h: box.height } : null;
}
// The view always has the canvas' aspect ratio, so the drawing fills it without letterboxing.
function setView(view) {
const size = canvasSize();
const v = { ...view };
v.h = size ? v.w * (size.h / size.w) : v.h || v.w * (page.h / page.w);
const slackX = Math.max(0, page.w - v.w);
const slackY = Math.max(0, page.h - v.h);
v.x = slackX ? Math.min(Math.max(v.x, -40), slackX + 40) : (page.w - v.w) / 2;
v.y = slackY ? Math.min(Math.max(v.y, -40), slackY + 40) : (page.h - v.h) / 2;
state.view = v;
chart.setAttribute('viewBox', `${v.x.toFixed(1)} ${v.y.toFixed(1)} ${v.w.toFixed(1)} ${v.h.toFixed(1)}`);
if (size) zoomLevel.textContent = `${Math.round((size.w / v.w) * 100)}%`;
}
function fitWidth() {
const size = canvasSize();
const width = size ? Math.max(page.w + 40, size.w / MAX_SCALE) : page.w;
fitMode = 'width';
setView({ x: (page.w - width) / 2, y: -20, w: width });
fitButton.title = 'Show the whole page';
}
function fitPage() {
const size = canvasSize();
const aspect = size ? size.h / size.w : page.h / page.w;
const width = Math.max(page.w, (page.h + 40) / aspect) + 40;
fitMode = 'page';
setView({ x: (page.w - width) / 2, y: 0, w: width });
fitButton.title = 'Fit the width';
}
const toggleFit = () => (fitMode === 'width' ? fitPage() : fitWidth());
function toChart(clientX, clientY) {
const matrix = chart.getScreenCTM();
if (!matrix) return [0, 0];
const p = new DOMPoint(clientX, clientY).matrixTransform(matrix.inverse());
return [p.x, p.y];
}
function zoomAt(factor, clientX, clientY) {
const v = state.view;
const size = canvasSize();
if (!size) return;
const scale = (size.w / v.w) * factor;
if (scale < (size.w / page.w) * MIN_ZOOM || scale > MAX_ZOOM) return;
const [px, py] = clientX == null ? [v.x + v.w / 2, v.y + v.h / 2] : toChart(clientX, clientY);
fitMode = 'custom';
setView({ x: px - (px - v.x) / factor, y: py - (py - v.y) / factor, w: v.w / factor });
}
chart.addEventListener('wheel', (e) => {
e.preventDefault();
if (e.ctrlKey || e.metaKey) {
zoomAt(Math.exp(-e.deltaY * 0.0025), e.clientX, e.clientY);
return;
}
const size = canvasSize();
if (!size) return;
const unit = state.view.w / size.w;
const dx = e.shiftKey && !e.deltaX ? e.deltaY : e.deltaX;
const dy = e.shiftKey && !e.deltaX ? 0 : e.deltaY;
setView({ ...state.view, x: state.view.x + dx * unit, y: state.view.y + dy * unit });
}, { passive: false });
chart.addEventListener('pointerdown', (e) => {
if (e.button !== 0) return;
state.drag = { x: e.clientX, y: e.clientY, view: { ...state.view }, moved: false, id: e.pointerId };
});
chart.addEventListener('pointermove', (e) => {
const drag = state.drag;
if (!drag || drag.id !== e.pointerId) return;
const dx = e.clientX - drag.x;
const dy = e.clientY - drag.y;
if (!drag.moved && Math.hypot(dx, dy) < 5) return;
if (!drag.moved) {
drag.moved = true;
chart.setPointerCapture(e.pointerId);
chart.classList.add('panning');
}
const scale = drag.view.w / chart.getBoundingClientRect().width;
fitMode = 'custom';
setView({ ...drag.view, x: drag.view.x - dx * scale, y: drag.view.y - dy * scale });
});
const endDrag = (e) => {
if (!state.drag || state.drag.id !== e.pointerId) return;
chart.classList.remove('panning');
// The click that follows a drag still needs to see `moved`.
window.setTimeout(() => { state.drag = null; }, 0);
};
chart.addEventListener('pointerup', endDrag);
chart.addEventListener('pointercancel', endDrag);
chart.addEventListener('dblclick', toggleFit);
const zoomLevel = h('span', { class: 'zoom-level', 'aria-live': 'polite' }, '100%');
const fitButton = h('button', { type: 'button', class: 'icon-btn', 'aria-label': 'Toggle between fitting the width and the whole page', title: 'Show the whole page', onclick: toggleFit }, '⤢');
const zoomControls = h('div', { class: 'zoom-controls' },
h('button', { type: 'button', class: 'icon-btn', 'aria-label': 'Zoom in', title: 'Zoom in (Ctrl + scroll)', onclick: () => zoomAt(1.25) }, '+'),
zoomLevel,
h('button', { type: 'button', class: 'icon-btn', 'aria-label': 'Zoom out', title: 'Zoom out (Ctrl + scroll)', onclick: () => zoomAt(1 / 1.25) }, '−'),
fitButton);
root.replaceChildren(
h('div', { class: 'map-top' }, strip, toolbar),
h('div', { class: 'map-canvas' }, chart, zoomControls),
tip,
);
fitWidth();
refresh();
layoutLegend();
layoutTags();
// Tag and legend widths depend on the web font, so measure again once it has loaded.
document.fonts.load('800 14px "Sofia Sans Semi Condensed"').then(() => requestAnimationFrame(relayout), () => {});
// Keep the same part of the diagram in view when the window or panel changes size.
new ResizeObserver(() => {
if (!canvasSize() || state.beforePrint) return;
if (fitMode === 'width') fitWidth();
else if (fitMode === 'page') fitPage();
else setView(state.view);
}).observe(chart);
// Long labels are squeezed to fit their box once real text widths are known.
function fitText() {
for (const [, el] of boxEls) {
const spans = el.header ? [el.text] : [...el.text.querySelectorAll('tspan')];
for (const span of spans) {
span.removeAttribute('textLength');
span.removeAttribute('lengthAdjust');
if (span.getComputedTextLength() > el.width) {
span.setAttribute('textLength', el.width.toFixed(1));
span.setAttribute('lengthAdjust', 'spacingAndGlyphs');
}
}
}
}
function relayout() {
layoutLegend();
layoutTags();
fitText();
}
return {
select(topic) {
state.selected = topic;
drawLinks();
},
show() {
if (fitMode === 'width') fitWidth();
relayout();
},
prepareForPrint() {
state.hover = null;
tip.hidden = true;
state.beforePrint = { view: { ...state.view }, fitMode };
state.view = { x: 0, y: 0, w: page.w, h: page.h };
chart.setAttribute('viewBox', `0 0 ${page.w} ${page.h}`);
relayout();
drawLinks();
},
afterPrint() {
if (!state.beforePrint) return;
fitMode = state.beforePrint.fitMode;
setView(state.beforePrint.view);
state.beforePrint = null;
},
layout,
};
}
+299
View File
@@ -0,0 +1,299 @@
// Turns the roadmap README into a topic tree with descriptions, resources and cross-links.
// Pure module: no DOM access, so it runs in the browser and under `node --test`.
export const TYPES = {
book: { icon: '📘', label: 'Book' },
video: { icon: '🎞️', label: 'Video' },
article: { icon: '📝', label: 'Article' },
link: { icon: '🔗', label: 'Link' },
audio: { icon: '🎧', label: 'Audio book' },
};
const TYPE_BY_SYMBOL = { '📘': 'book', '🎞': 'video', '📝': 'article', '🔗': 'link', '🎧': 'audio' };
// Heading prefixes the README uses only to mark the heading level.
const LEVEL_MARKERS = new Set(['✳', '🔵', '🔶', '🔸']);
const SYMBOLS = /^[\p{Extended_Pictographic}\uFE0F\u200D\u200C\u20E3\s]+/u;
const INVISIBLE = /[\uFE0F\u200D\u200C\u20E3\s]/u;
const URL_PART = String.raw`https?:\/\/[^\s()]+(?:\([^\s()]*\)[^\s()]*)*`;
const HEADING = /^(#{1,6})\s+(.+)$/;
const RESOURCE = new RegExp(String.raw`^\s*[-*]\s+\[(.+)\]\((${URL_PART})\)(.*)$`, 'u');
const HEADING_LINK = new RegExp(String.raw`^\[(.+)\]\((${URL_PART})\)$`, 'u');
const BULLET = /^\s*[-*]\s+(.+)$/;
const SKIP = /^(\s*>|<!--|!\[|\[!\[|_{3,}\s*$|\|)/;
function splitSymbols(text) {
const match = text.match(SYMBOLS);
const prefix = match ? match[0] : '';
return {
prefix: prefix.trim(),
symbols: [...prefix].filter((c) => !INVISIBLE.test(c)),
rest: text.slice(prefix.length).replace(/\s+/g, ' ').trim(),
};
}
function headingInfo(text) {
const link = text.match(HEADING_LINK);
const { prefix, symbols, rest } = splitSymbols(link ? link[1] : text);
const generic = symbols.every((s) => LEVEL_MARKERS.has(s));
return { title: rest, icon: generic ? '' : prefix, link: link ? link[2] : null, label: link ? link[1] : null };
}
function makeResource(label, url, trailing = '') {
const { symbols, rest } = splitSymbols(label);
let type = 'link';
for (const s of symbols) if (TYPE_BY_SYMBOL[s]) type = TYPE_BY_SYMBOL[s];
let domain = '';
try {
domain = new URL(url).hostname.replace(/^www\./, '');
} catch {
domain = '';
}
return {
title: rest,
url,
domain,
type,
beginner: symbols.includes('👶'),
gem: symbols.includes('💎'),
note: trailing.replace(/^\s*[-–—:]\s*/, '').trim(),
topic: null,
sharedWith: [],
};
}
export function slugify(text) {
return text
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/\+\+/g, 'pp')
.replace(/#/g, 'sharp')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
function normalizeUrl(url) {
try {
const u = new URL(url);
return `${u.hostname.replace(/^www\./, '').toLowerCase()}${u.pathname.replace(/\/+$/, '')}${u.search}`;
} catch {
return url;
}
}
function makeNode(fields) {
return { title: '', icon: '', depth: 0, group: '', parent: null, children: [], resources: [], description: [], ...fields };
}
export function parseRoadmap(markdown) {
const lines = markdown.replace(/\r\n?/g, '\n').split('\n');
const root = makeNode({ id: 'root', slug: '', title: 'Embedded Engineering Roadmap' });
const groups = [];
const stack = [];
let target = root;
let topic = null;
let paragraph = [];
const flush = () => {
if (paragraph.length && target) target.description.push({ type: 'p', text: paragraph.join(' ') });
paragraph = [];
};
for (const line of lines) {
const heading = line.match(HEADING);
if (heading) {
flush();
const level = heading[1].length;
if (level === 1) continue;
const info = headingInfo(heading[2].trim());
if (level === 2) {
const group = makeNode({ title: info.title, icon: info.icon, topics: [] });
groups.push(group);
stack.length = 0;
target = group;
topic = null;
continue;
}
while (stack.length && stack[stack.length - 1].level >= level) stack.pop();
const parent = stack.length ? stack[stack.length - 1].node : root;
const group = groups[groups.length - 1];
const node = makeNode({ title: info.title, icon: info.icon, depth: parent.depth + 1, group: group ? group.title : '', parent });
if (info.link) node.resources.push(makeResource(info.label, info.link));
parent.children.push(node);
if (group && parent === root) group.topics.push(node);
stack.push({ level, node });
target = node;
topic = node;
continue;
}
const resource = line.match(RESOURCE);
if (resource) {
flush();
if (topic) topic.resources.push(makeResource(resource[1], resource[2], resource[3]));
continue;
}
if (SKIP.test(line)) {
flush();
continue;
}
const bullet = line.match(BULLET);
if (bullet) {
flush();
if (!target) continue;
const last = target.description[target.description.length - 1];
if (last && last.type === 'ul') last.items.push(bullet[1].trim());
else target.description.push({ type: 'ul', items: [bullet[1].trim()] });
continue;
}
if (!line.trim()) {
flush();
continue;
}
// Text before the first "##" heading after the badges is the README introduction.
if (target === root && root.description.length >= 2) continue;
paragraph.push(line.trim());
}
flush();
const topics = [];
const used = new Set(['']);
const walk = (node, path) => {
for (const child of node.children) {
let slug = slugify(child.title) || 'topic';
if (used.has(slug)) slug = `${slug}-${slugify(node.title)}`;
for (let i = 2; used.has(slug); i += 1) slug = `${slugify(child.title)}-${i}`;
used.add(slug);
child.slug = slug;
child.id = slug;
child.path = path;
for (const r of child.resources) r.topic = child;
topics.push(child);
walk(child, [...path, child.title]);
}
};
walk(root, []);
const total = (node) => {
node.total = node.resources.length + node.children.reduce((sum, c) => sum + total(c), 0);
return node.total;
};
total(root);
const order = new Map(topics.map((t, i) => [t, i]));
const linkMap = new Map();
const linkFor = (a, b) => {
const [source, target] = order.get(a) <= order.get(b) ? [a, b] : [b, a];
const key = `${source.id}|${target.id}`;
if (!linkMap.has(key)) linkMap.set(key, { source, target, shared: [], mentions: [] });
return linkMap.get(key);
};
const byUrl = new Map();
for (const node of topics) {
for (const r of node.resources) {
const key = normalizeUrl(r.url);
if (!byUrl.has(key)) byUrl.set(key, []);
byUrl.get(key).push(r);
}
}
for (const list of byUrl.values()) {
const owners = [...new Set(list.map((r) => r.topic))];
if (owners.length < 2) continue;
for (const r of list) r.sharedWith = owners.filter((o) => o !== r.topic);
for (let i = 0; i < owners.length; i += 1) {
for (let j = i + 1; j < owners.length; j += 1) linkFor(owners[i], owners[j]).shared.push(list[0].title);
}
}
const matchers = topics.map((t) => ({ topic: t, patterns: aliases(t.title).map(aliasPattern) }));
for (const from of topics) {
const texts = [
...from.resources.map((r) => `${r.title} ${r.note}`),
...from.description.flatMap((b) => (b.type === 'p' ? [b.text] : b.items)),
];
for (const { topic: to, patterns } of matchers) {
if (to === from || isLineage(from, to)) continue;
for (const text of texts) {
if (patterns.some((p) => p.test(text))) linkFor(from, to).mentions.push({ topic: from, text: plainText(text) });
}
}
}
const links = [...linkMap.values()];
for (const link of links) link.weight = link.shared.length * 2 + link.mentions.length;
for (const t of topics) t.links = [];
for (const link of links) {
link.source.links.push(link);
link.target.links.push(link);
}
return {
root,
topics,
groups: groups.filter((g) => g.topics.length),
links,
bySlug: new Map(topics.map((t) => [t.slug, t])),
stats: { topics: topics.length, resources: byUrl.size, links: links.length },
};
}
// Topic names too generic to count as a reference when they appear in another topic's text.
const ALIAS_STOPLIST = new Set(['Electronics', 'Make', 'Projects', 'Testing', 'Logic', 'Sensors', 'Actuators', 'Search and Ask!']);
function aliases(title) {
const found = new Set([title]);
const base = title.replace(/\s*\(([^)]*)\)\s*/g, (m, inner) => {
found.add(inner.trim());
return ' ';
}).trim();
found.add(base);
for (const part of base.split(' / ')) found.add(part.trim());
return [...found].filter((a) => !ALIAS_STOPLIST.has(a) && (a.length >= 3 || /^[A-Z0-9]{2,}$/.test(a)));
}
function aliasPattern(alias) {
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`(?<![\\w+#-])${escaped}(?![\\w+#-])`, 'u');
}
function isLineage(a, b) {
for (let n = a; n; n = n.parent) if (n === b) return true;
for (let n = b; n; n = n.parent) if (n === a) return true;
return false;
}
function plainText(markdown) {
return markdown.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1').replace(/[*_`$]/g, '').replace(/\s+/g, ' ').trim();
}
const ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
const GREEK = { Pi: 'Π', pi: 'π', lambda: 'λ' };
export function escapeHtml(text) {
return text.replace(/[&<>"']/g, (c) => ESCAPES[c]);
}
function formatText(escaped) {
return escaped
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/(^|[\s(])_(.+?)_(?=[\s.,;:)!?]|$)/g, '$1<em>$2</em>')
.replace(/\$\$\\(\w+)\$\$/g, (m, name) => GREEK[name] || name);
}
// Renders README inline markdown to HTML. Input is escaped first; only http(s) links become anchors.
export function renderInline(markdown) {
const link = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
let html = '';
let last = 0;
for (const m of markdown.matchAll(link)) {
html += formatText(escapeHtml(markdown.slice(last, m.index)));
html += `<a href="${escapeHtml(m[2])}" target="_blank" rel="noopener noreferrer">${formatText(escapeHtml(m[1]))}</a>`;
last = m.index + m[0].length;
}
return html + formatText(escapeHtml(markdown.slice(last)));
}
+90
View File
@@ -0,0 +1,90 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { parseRoadmap, renderInline } from './parser.js';
import { linkDiagram } from './diagram.js';
import { SPACE, buildLayout } from './layout.js';
const data = parseRoadmap(readFileSync(new URL('../README.md', import.meta.url), 'utf8'));
const find = (title) => data.topics.find((t) => t.title === title);
const linked = (a, b) => data.links.find((l) => (l.source === a && l.target === b) || (l.source === b && l.target === a));
test('builds the topic tree from the README headings', () => {
assert.ok(data.topics.length > 100);
assert.equal(find('Microcontrollers').depth, 1);
assert.ok(find('Microcontrollers').children.some((c) => c.title === 'GPIO'));
assert.equal(find('1-Wire').parent.title, 'Basic Protocols');
assert.equal(find('High-Performance Computing (HPC)').parent.title, 'Multithreading & Parallel Processing');
assert.equal(find('History'), undefined);
assert.deepEqual(data.groups.map((g) => g.title), ["Don't Know Where to Start!", 'Learning Resources']);
});
test('gives every topic a unique slug', () => {
assert.equal(new Set(data.topics.map((t) => t.slug)).size, data.topics.length);
});
test('parses resource markers, bracketed titles and URLs with parentheses', () => {
const beej = find('C').resources.find((r) => r.title.startsWith("Beej's Guide"));
assert.deepEqual([beej.type, beej.beginner, beej.gem], ['book', true, false]);
const j1939 = find('CAN').resources.find((r) => r.title.startsWith('J1939'));
assert.equal(j1939.title, 'J1939 Explained - A Simple Intro [v2.0 | 2021]');
assert.match(find('PCM').resources[0].url, /_04\(1\)\.pdf$/);
assert.equal(find('GoogleTest - Google Testing and Mocking Framework').resources[0].url, 'https://github.com/google/googletest');
assert.match(find('Career Development').resources[0].note, /job board/);
});
test('keeps descriptions and the README introduction', () => {
assert.match(data.root.description[0].text, /^This roadmap is designed/);
assert.match(find('Microcontrollers').description[0].text, /^Microcontrollers are integrated circuits/);
});
test('links topics that share a resource or mention each other', () => {
assert.ok(linked(find('Docker'), find('CI/CD Pipelines')).shared.length > 0);
assert.ok(linked(find('I2C'), find('Sensors')).mentions.length > 0);
assert.equal(linked(find('Microcontrollers'), find('GPIO')), undefined, 'parent and child are not cross-linked');
assert.equal(linked(find('Thread'), find('RT-Thread')), undefined, 'RT-Thread is not a mention of Thread');
});
test('renderInline escapes HTML and only links http(s) URLs', () => {
assert.equal(
renderInline('<b>x</b> **y** [a](https://e.com/?q=1&r=2) [j](javascript:alert(1))'),
'&lt;b&gt;x&lt;/b&gt; <strong>y</strong> <a href="https://e.com/?q=1&amp;r=2" target="_blank" rel="noopener noreferrer">a</a> [j](javascript:alert(1))',
);
});
const map = JSON.parse(readFileSync(new URL('./map.json', import.meta.url), 'utf8'));
test('places topics in the map areas and their cross-section', () => {
const plan = linkDiagram(map, data);
assert.deepEqual(plan.unmatched, [], 'every map topic matches a README heading (or an alias in diagram.js)');
assert.deepEqual(plan.invalid, [], 'importance is required, recommended or possible');
assert.deepEqual(find('GPIO').areas, ['SOFTWARE', 'HARDWARE']);
assert.deepEqual(find('I2C').areas, ['SOFTWARE', 'HARDWARE']);
assert.deepEqual(find('Sensors & Actuators').areas, ['SOFTWARE', 'HARDWARE']);
assert.deepEqual(find('GDB').areas, ['SOFTWARE']);
assert.deepEqual(find('Oscilloscope').areas, ['HARDWARE']);
assert.deepEqual(find('Soft Skills').areas, ['SOFT SKILLS']);
assert.deepEqual(find('Microcontrollers').areas, ['SOFTWARE', 'HARDWARE']);
assert.equal(find('RTOS Basics').importance, 'required');
assert.equal(find('Rust').importance, 'possible');
assert.deepEqual(find('Linux Device Drivers').areas, ['SOFTWARE']);
assert.equal(find('Projects').areas.length, 0, 'getting-started topics are not on the map');
});
test('lays the map out on one A4 page with a minimum spacing between boxes', () => {
const layout = buildLayout(linkDiagram(map, data));
assert.deepEqual(layout.missing, [], 'every cluster in map.json has a place in layout.js');
assert.ok(layout.height / layout.width <= Math.SQRT2, 'fits A4 portrait');
const boxes = layout.items.filter((i) => i.kind === 'box');
const inside = (a, b) => a.x >= b.x && a.y >= b.y && a.x + a.w <= b.x + b.w && a.y + a.h <= b.y + b.h;
const gap = (a, b) => Math.max(b.x - a.x - a.w, a.x - b.x - b.w, b.y - a.y - a.h, a.y - b.y - b.h);
for (const [i, a] of boxes.entries()) {
for (const b of boxes.slice(i + 1)) assert.ok(gap(a, b) >= SPACE - 0.01, `${a.text} / ${b.text}`);
for (const g of layout.groups) {
if (!inside(a, g.rect)) continue;
const r = g.rect;
const d = Math.min(a.x - r.x, a.y - r.y, r.x + r.w - a.x - a.w, r.y + r.h - a.y - a.h);
assert.ok(d >= SPACE - 0.01, `${a.text} too close to the ${g.name} outline`);
}
}
});
+1148
View File
File diff suppressed because it is too large Load Diff
+78
View File
@@ -0,0 +1,78 @@
// Renders the roadmap map to an A4 PDF and a PNG with headless Chromium, straight from the site.
// Usage: node site/tools/render.mjs [rootDir] [pagePath] [outDir]
// defaults: repository root, /site/, ./dist (needs: npm install --no-save playwright)
import { createServer } from 'node:http';
import { mkdir, readFile } from 'node:fs/promises';
import { extname, join, resolve, sep } from 'node:path';
import { chromium } from 'playwright';
const root = resolve(process.argv[2] || '.');
const pagePath = process.argv[3] || '/site/';
const outDir = resolve(process.argv[4] || 'dist');
const NAME = 'Embedded-Engineering-Roadmap';
const PNG_SCALE = 2;
const TYPES = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json',
'.md': 'text/markdown; charset=utf-8',
'.woff2': 'font/woff2',
'.svg': 'image/svg+xml',
'.png': 'image/png',
};
const server = createServer(async (req, res) => {
const url = new URL(req.url, 'http://localhost');
const file = resolve(join(root, decodeURIComponent(url.pathname), url.pathname.endsWith('/') ? 'index.html' : ''));
if (file !== root && !file.startsWith(root + sep)) {
res.writeHead(403).end();
return;
}
try {
const body = await readFile(file);
res.writeHead(200, { 'content-type': TYPES[extname(file)] || 'application/octet-stream' }).end(body);
} catch {
res.writeHead(404).end();
}
});
await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
const url = `http://127.0.0.1:${server.address().port}${pagePath}`;
const browser = await chromium.launch();
try {
// bypassCSP lets the PNG step add one style rule; the site itself only allows its own stylesheets.
const context = await browser.newContext({ viewport: { width: 1400, height: 1000 }, deviceScaleFactor: PNG_SCALE, bypassCSP: true });
const page = await context.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
await page.goto(url, { waitUntil: 'networkidle' });
await page.waitForSelector('.diagram .box');
await page.evaluate(async () => {
await document.fonts.ready;
await new Promise((ok) => requestAnimationFrame(() => requestAnimationFrame(ok)));
});
if (errors.length) throw new Error(`The page reported errors:\n${errors.join('\n')}`);
await mkdir(outDir, { recursive: true });
await page.evaluate(() => window.dispatchEvent(new Event('beforeprint')));
await page.emulateMedia({ media: 'print' });
await page.pdf({ path: join(outDir, `${NAME}.pdf`), format: 'A4', printBackground: true, preferCSSPageSize: true });
// PNG: the same print rendering, at the diagram's own size instead of the page's.
// page.pdf() fires afterprint, which restores the screen view, so prepare for print again.
const { w, h } = await page.evaluate(() => {
const bg = document.querySelector('.diagram .page-bg');
return { w: Number(bg.getAttribute('width')), h: Number(bg.getAttribute('height')) };
});
await page.setViewportSize({ width: Math.ceil(w) + 40, height: Math.ceil(h) + 40 });
await page.addStyleTag({ content: `.diagram { width: ${w}px !important; height: ${h}px !important; max-height: none !important; }` });
await page.evaluate(() => window.dispatchEvent(new Event('beforeprint')));
await page.locator('.diagram').screenshot({ path: join(outDir, `${NAME}.png`) });
console.log(`Wrote ${NAME}.pdf and ${NAME}.png (${Math.round(w * PNG_SCALE)}x${Math.round(h * PNG_SCALE)}) to ${outDir}`);
} finally {
await browser.close();
server.close();
}