Compare commits

..
289 Commits
Author SHA1 Message Date
Meysam Parvizi b6dec3d394 feat(site): add Display & Camera cluster (MIPI CSI-2, MIPI DSI) to the map 2026-10-10 04:57:24 +02:00
Meysam Parvizi da88157952 fix(site): soften cluster header hover and selected highlight 2026-10-10 04:38:48 +02:00
Meysam Parvizi 3150cc358d docs: add free soft-skill resources 2026-10-10 04:36:03 +02:00
Meysam Parvizi b08f3a9699 docs: add resources for UWB, Profinet, LIN, MOST, FlexRay and the six soft skills 2026-10-10 04:16:23 +02:00
Meysam Parvizi 8cf40b4993 fix(site): make cluster headers clickable with pill feedback instead of link-style underline; name the hovered box in the tooltip 2026-10-10 04:05:19 +02:00
Meysam Parvizi 95aff195b4 Move README heading names into map.json, warn about unmapped README sections, document the README-explorer workflow 2026-10-10 03:55:37 +02:00
Meysam Parvizi 2c4faffbd8 feat(site): pinch to zoom and two-finger pan on the map
Two touches zoom around their midpoint and pan with it; lifting them does not open a topic. Safari's trackpad pinch gesture events are handled too.
2026-10-10 03:46:13 +02:00
Meysam Parvizi 558cc859f5 fix(site): render the map without transitions when printing
The box strokes fade from screen to print colors over 0.15 s, and the PNG/PDF render sometimes captured them mid-fade. That made the output differ slightly between runs and the map fingerprint flip, which created an unnecessary release (v2.0.2). Print styles now disable transitions and animations, so 12 of 12 test renders are byte-identical.
2026-10-10 03:10:24 +02:00
Meysam Parvizi 71adde583f fix(site): highlight group labels by their pill on hover instead of underlining 2026-10-10 02:56:48 +02:00
Meysam Parvizi 334e7d0456 docs(readme): introduce the interactive roadmap and link the image to it
Add a short description of the interactive explorer above the roadmap image and make the image open the site.
2026-10-10 02:56:48 +02:00
Meysam Parvizi 4e0ba15dc9 style(site): match the logo height to the license badge in the map footer 2026-10-10 02:41:39 +02:00
Meysam Parvizi 4c52895477 feat(site): use curated connections between topics
Connections come from the links list in map.json instead of only from shared or mentioned resources. Curated ones are solid lines with a reason in the details panel; the text-based ones stay as faint dashed evidence. Tests check names, duplicates and that every technical topic is connected.
2026-10-10 02:39:06 +02:00
Meysam Parvizi 2e2ca453c9 docs(map): draft the connections between topics
A first set of 196 hand-picked connections, each with a one-line reason, for review by the maintainer.
2026-10-10 02:39:06 +02:00
Meysam Parvizi b83bdda42f style(site): make the hover tooltip semi-transparent
The popup background is 72% opaque with a slight blur, so the map stays visible behind it.
2026-10-10 02:37:25 +02:00
Meysam Parvizi 8704cf6234 fix(site): allow zooming in after "Show the whole page"
The whole-page view sits below the minimum zoom, so every zoom-in step was rejected as out of range. Limits now only stop steps that go further out of range, and zooming out can reach the whole-page view.
2026-10-10 02:35:06 +02:00
Meysam Parvizi ad4a5406d9 fix(site): make the tooltip hint look like a hint, not a link
The tooltip ignores the mouse and follows the cursor, so its hint is now plain muted text about the topic instead of an accent-colored link-like line.
2026-10-10 01:58:55 +02:00
Meysam Parvizi 2c70f347de fix(site): no scrollbar over the chip rows in Firefox
Chip rows wrap on desktop; on phones they swipe with a hidden scrollbar and an edge fade.
2026-10-10 01:52:43 +02:00
Meysam Parvizi 752f36a921 ci: give the release job a static name 2026-10-10 01:41:51 +02:00
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
35 changed files with 6669 additions and 922 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
+146
View File
@@ -0,0 +1,146 @@
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: Warn about README sections that are not on the map
run: node site/tools/coverage.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: Publish release
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/).
+52
View File
@@ -0,0 +1,52 @@
# 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.
- Connections between topics on the map are listed in `site/map.json` under `links` as `["Topic", "Other topic", "why they are related"]`, using the names shown on the map. Connect topics that a learner should study together or that depend on each other, and keep the reason to one short sentence. Tests fail if a name is misspelled, a pair is listed twice or a topic has no connection.
## 6. How the README Feeds the Interactive Roadmap
The [interactive roadmap](https://m3y54m.github.io/Embedded-Engineering-Roadmap/) reads `README.md` directly; there is no other copy of the content. Most contributions need no change outside the README.
- **Topics** come from heading depth: `##` is a group, `###` a main topic, `####` and deeper are subtopics. The emojis in headings (✳️ 🔵 🔶 🔸) are decoration only. Renaming a heading changes its link (`#/topic-name`).
- **Resources** are list items in the form `- [📘👶💎 Title](https://url)`, optionally followed by a dash and a short note. Symbols: 📘 book, 🎞️ video, 📝 article, 🔗 link, 🎧 audio, 👶 beginner, 💎 essential. `python3 .github/scripts/check_readme.py` checks the format and duplicates.
- **The map** (`site/map.json`) draws only the main topics. Each label on it opens the README heading with the same name. If the wording differs, or the label has no section of its own, add `"Label": ["README heading"]` to `readme` in `site/map.json`. A new main topic needs a README section and a box in `site/map.json` (and at least one entry in `links`), unless it should stay off the map: then list its title or group in `offMap`. CI warns about main topics that are neither.
- **Preview and test locally:** `python3 -m http.server 8765` from the repository root, then open `/site/`; `node --test site/parser.test.mjs` runs the checks CI runs.
## 7. 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.
+1366 -922
View File
File diff suppressed because it is too large Load Diff
+697
View File
@@ -0,0 +1,697 @@
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))));
}
// Hand-picked connections of a topic (from map.json), each with the reason it is listed.
function relatedTopics(node) {
const seen = new Set();
const items = [];
for (const box of node.boxes || []) {
for (const { box: other, reason } of box.related || []) {
if (seen.has(other)) continue;
seen.add(other);
items.push({ other, reason });
}
}
return items;
}
function curatedList(items) {
return h('ul', { class: 'related' }, items.map(({ other, reason }) => {
const target = other.topics[0];
return h('li', {}, h('button', { type: 'button', onclick: () => navigate(target, { reveal: true }) },
swatch(target),
h('span', {}, other.text),
h('span', { class: 'why' }, reason)));
}));
}
function related(node, skip = new Set()) {
const items = [...node.links]
.filter((link) => !skip.has(link.source === node ? link.target : link.source))
.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', {}, state.plan ? state.plan.connections.filter((c) => c.reason).length : 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 solid lines to its related topics. Fainter dashed lines come from resources that share or mention a topic.'),
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,
relatedTopics(node).length ? h('span', { class: 'pill' }, `${relatedTopics(node).length} related`) : 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)));
const curated = relatedTopics(node);
if (curated.length) panel.append(section('Related topics', curatedList(curated)));
const listed = new Set(curated.flatMap(({ other }) => other.topics));
const evidence = node.links.filter((link) => !listed.has(link.source === node ? link.target : link.source));
if (evidence.length) panel.append(section(curated.length ? 'Also linked by resources' : 'Linked by resources', related(node, listed)));
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

+150
View File
@@ -0,0 +1,150 @@
// 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.
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);
// map.json "readme": map labels whose README heading is worded differently (or has no section of its own).
const aliases = map.readme || {};
const resolve = (text) => {
if (Object.hasOwn(aliases, text)) return [aliases[text]].flat().map((title) => index.get(key(title))).filter(Boolean);
const topic = index.get(key(text));
return topic ? [topic] : [];
};
// 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) }));
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;
// a topic that is not drawn itself but has drawn subtopics (Soft Skills) takes theirs.
const fromChildren = new Map();
for (const t of data.topics) {
if (!t.areas.length) continue;
for (let p = t.parent; p; p = p.parent) {
if (!fromChildren.has(p)) fromChildren.set(p, new Set());
for (const area of t.areas) fromChildren.get(p).add(area);
}
}
for (const t of data.topics) {
if (!t.areas.length && fromChildren.has(t)) t.areas = [...fromChildren.get(t)];
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;
};
// A connection joins two boxes. `reason` is set for the hand-picked ones in map.json; the rest
// are evidence only (a shared resource, or one topic's text naming another).
const pairs = new Map();
const pairFor = (a, b) => {
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, reason: null, weight: 0, shared: 0, links: [] });
return pairs.get(id);
};
const topicBoxes = new Map(boxes.filter((b) => !b.header).map((b) => [b.text, b]));
const invalidLinks = [];
for (const box of boxes) box.related = [];
for (const [from, to, reason] of map.links || []) {
const a = topicBoxes.get(from);
const b = topicBoxes.get(to);
if (!a || !b || a === b || typeof reason !== 'string' || !reason.trim()) {
invalidLinks.push(`${from} - ${to}`);
continue;
}
const pair = pairFor(a, b);
if (pair.reason) {
invalidLinks.push(`${from} - ${to} (listed twice)`);
continue;
}
pair.reason = reason.trim();
a.related.push({ box: b, reason: pair.reason });
b.related.push({ box: a, reason: pair.reason });
}
for (const link of data.links) {
const a = boxFor(link.source);
const b = boxFor(link.target);
if (!a || !b || a === b) continue;
const pair = pairFor(a, b);
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),
invalidLinks,
invalidReadme: Object.entries(aliases).flatMap(([label, titles]) => {
const onMap = boxes.some((b) => b.text === label) || label in map.groups;
const missing = [titles].flat().filter((title) => !index.has(key(title)));
return onMap && !missing.length ? [] : [`${label} -> ${[titles].flat().join(' + ')}`];
}), 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>
+168
View File
@@ -0,0 +1,168 @@
// 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',
'display',
'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', 'display'], 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,
};
}
+536
View File
@@ -0,0 +1,536 @@
/* 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: wrap;
align-items: center;
gap: 6px;
}
.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;
}
.header-bg {
fill: transparent;
transition: fill 0.15s;
}
.box.header.target:hover .header-bg,
.box.header.selected .header-bg {
fill: color-mix(in srgb, var(--accent) 14%, transparent);
}
.group-tag.target:hover .tag-bg {
fill: color-mix(in srgb, var(--surface-solid) 85%, var(--accent));
stroke-width: 3;
}
.box.selected .box-bg {
stroke: var(--text);
stroke-width: 3;
}
.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-bg {
stroke: var(--accent);
stroke-width: 2;
}
.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.evidence,
.link-all.evidence {
stroke-dasharray: 5 5;
stroke-opacity: 0.45;
}
.link-end {
fill: var(--accent);
}
.link-all {
stroke-opacity: 0.4;
}
.link-all.curated {
stroke-width: 1.2px;
stroke-opacity: 0.28;
}
.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;
}
.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;
}
/* One swipeable row per strip; the fade at the right edge shows there is more. */
.start-strip,
.map-toolbar {
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: none;
padding-right: 28px;
mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
}
.start-strip::-webkit-scrollbar,
.map-toolbar::-webkit-scrollbar {
display: 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 {
/* Print the final colors, not a frame of the screen-to-print color transition. */
*,
*::before,
*::after {
transition: none !important;
animation: none !important;
}
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;
}
}
+519
View File
@@ -0,0 +1,519 @@
{
"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"]
]
},
"display": {
"header": "Display & Camera",
"per": 2,
"topics": [
["MIPI CSI-2", "possible"],
["MIPI DSI", "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"]
]
}
}
}
],
"links": [
["C", "Memory Management", "Pointers, the stack and the heap are managed by hand in C"],
["C++", "Memory Management", "Ownership, RAII and smart pointers decide who frees memory"],
["Rust", "Memory Management", "Ownership and borrowing replace manual memory management"],
["Python", "Memory Management", "Garbage collection; MicroPython has to fit in small RAM"],
["C", "C++", "C++ builds on C, and both are mixed in one firmware project"],
["C", "Compilers / GCC", "The toolchain that turns C code into firmware"],
["C", "Linux Kernel", "The kernel and its drivers are written in C"],
["C", "GDB", "Source-level debugging of C programs"],
["C++", "Design Patterns", "Classic object-oriented patterns are taught in C++"],
["C++", "Qt Framework", "Qt is a C++ framework"],
["Python", "Bash Scripting", "Both automate tooling, tests and build tasks"],
["Python", "SIL / HIL Testing", "Test rigs and HIL benches are often scripted in Python"],
["Python", "Edge AI", "Models are usually trained in Python before deployment"],
["Rust", "Embedded Security", "Memory safety removes a whole class of vulnerabilities"],
["Assembly", "Computer Architecture", "Instructions map directly to registers and the memory model"],
["Assembly", "Compilers / GCC", "Reading compiler output and writing startup code"],
["Assembly", "Interrupts", "Vector tables and context saving are done in assembly"],
["Algorithms & Data Structures", "Memory Management", "The data structure you pick decides memory use and allocation"],
["Algorithms & Data Structures", "Digital Signal Processing", "Filters and transforms are algorithms with tight time and memory budgets"],
["Algorithms & Data Structures", "Problem-Solving Skills & Critical Thinking", "Choosing and breaking down an approach"],
["Design Patterns", "State Machines", "The State pattern; many embedded designs are state machines"],
["State Machines", "Interrupts", "Events and interrupts drive state transitions"],
["State Machines", "RTOS Basics", "RTOS tasks are usually written as event-driven state machines"],
["State Machines", "Digital Design", "Finite-state machines are also the core of sequential logic in hardware"],
["JTAG / SWD", "OpenOCD", "OpenOCD drives JTAG/SWD debug probes"],
["JTAG / SWD", "GDB", "GDB reaches the target through a JTAG/SWD probe"],
["JTAG / SWD", "Bootloader / DFU", "Flashes the first bootloader and recovers bricked boards"],
["OpenOCD", "GDB", "OpenOCD is the GDB server for most microcontrollers"],
["GDB", "Compilers / GCC", "GDB reads the debug information the compiler emits"],
["GDB", "Problem-Solving Skills & Critical Thinking", "Debugging is structured problem solving"],
["Agile / SCRUM", "V-Model", "Two common process models; safety projects often combine them"],
["Agile / SCRUM", "CI/CD Pipelines", "Short iterations rely on automated builds and tests"],
["Agile / SCRUM", "Teamwork & Collaborative Abilities", "Scrum is built around close teamwork"],
["Agile / SCRUM", "Organizational & Time Management Skills", "Sprint planning and prioritising work"],
["V-Model", "Standards & Certifications", "Certified projects follow V-model verification"],
["V-Model", "SIL / HIL Testing", "Integration and system testing on SIL and HIL benches"],
["Git", "SVN", "Both are version control systems; Git is the common choice today"],
["Git", "CI/CD Pipelines", "Pipelines start from commits and pull requests"],
["Git", "Teamwork & Collaborative Abilities", "Branches and code review are how teams share code"],
["Communication Skills", "Teamwork & Collaborative Abilities", "Collaboration depends on clear communication"],
["Compilers / GCC", "Make / CMake", "Build systems drive the compiler and linker"],
["Make / CMake", "CI/CD Pipelines", "Pipelines run the same build commands"],
["Make / CMake", "Buildroot / Yocto", "Buildroot and Yocto are build systems for whole Linux images"],
["Bash Scripting", "CI/CD Pipelines", "Most pipeline steps are shell commands"],
["Bash Scripting", "Operating System Fundamentals", "The shell and processes are core OS concepts"],
["Docker", "CI/CD Pipelines", "Containers give pipelines a reproducible toolchain"],
["Docker", "Buildroot / Yocto", "Yocto builds are commonly run in containers"],
["TDD & Unit Testing", "CI/CD Pipelines", "Tests run automatically on every change"],
["TDD & Unit Testing", "Hardware Simulation / Emulation", "Tests run on a host against simulated hardware"],
["SIL / HIL Testing", "Hardware Simulation / Emulation", "SIL runs the firmware against simulated hardware"],
["SIL / HIL Testing", "CI/CD Pipelines", "HIL rigs can run as pipeline stages"],
["Standards & Certifications", "Embedded Security", "Security standards such as IEC 62443"],
["Standards & Certifications", "AUTOSAR", "Automotive software is developed to ISO 26262"],
["Standards & Certifications", "PCB Design / EMC", "EMC compliance testing is a certification requirement"],
["Embedded Security", "Bootloader / DFU", "Secure boot and signed firmware updates"],
["Embedded Security", "IoT", "Connected devices are exposed to attacks and need secure updates"],
["Embedded Security", "TCP/IP", "TLS runs on top of TCP/IP"],
["Embedded GUI", "Qt Framework", "Qt is a common GUI toolkit on embedded Linux"],
["Embedded GUI", "SPI", "Small displays are often driven over SPI"],
["Embedded GUI", "DMA", "DMA moves frame buffers to the display"],
["IoT", "MQTT", "A common messaging protocol for IoT devices"],
["IoT", "CoAP", "A lightweight request/response protocol for constrained devices"],
["IoT", "Wi-Fi", "A typical uplink for mains-powered devices"],
["IoT", "LoRa", "Long-range, low-power links"],
["IoT", "NB-IoT", "Cellular connectivity for low-power devices"],
["IoT", "Power Management", "Battery-powered nodes need low-power design"],
["Edge AI", "Digital Signal Processing", "Features are extracted from sensor signals before inference"],
["Edge AI", "Sensors & Actuators", "Models run on sensor data"],
["Edge AI", "Power Management", "Inference has to fit a small energy budget"],
["Edge AI", "FPGA Development", "FPGAs are used as inference accelerators"],
["AUTOSAR", "CAN", "AUTOSAR communication stacks run over CAN"],
["AUTOSAR", "Ethernet", "Automotive Ethernet is part of the AUTOSAR stack"],
["AUTOSAR", "RTOS Basics", "AUTOSAR OS is an OSEK-based real-time operating system"],
["Operating System Fundamentals", "RTOS Basics", "Scheduling, tasks and synchronisation carry over"],
["Operating System Fundamentals", "Linux Kernel", "Processes, memory and drivers as implemented in Linux"],
["Operating System Fundamentals", "Threading / Parallelism", "Threads and scheduling basics"],
["Operating System Fundamentals", "Memory Management", "Virtual memory and allocators"],
["Operating System Fundamentals", "Computer Architecture", "MMU, privilege levels and caches"],
["Linux Kernel", "Device Drivers", "Drivers are kernel modules"],
["Linux Kernel", "U-Boot", "U-Boot loads the kernel and the device tree"],
["Linux Kernel", "Buildroot / Yocto", "The kernel is configured and built as part of the image"],
["Linux Kernel", "Memory Technologies & File Systems", "Flash-friendly file systems and block layers"],
["Device Drivers", "I2C", "Linux drivers talk to devices over buses like I2C"],
["Device Drivers", "SPI", "Linux drivers talk to devices over buses like SPI"],
["Device Drivers", "GPIO", "Pins are exposed to drivers through the GPIO subsystem"],
["Device Drivers", "Interrupts", "Drivers handle device interrupts"],
["Device Drivers", "USB", "The USB stack needs host and device drivers"],
["Device Drivers", "PCIe", "PCIe devices need kernel drivers"],
["U-Boot", "Bootloader / DFU", "The same job on Linux boards: initialise hardware and load the OS"],
["U-Boot", "Buildroot / Yocto", "Both build and ship the bootloader with the image"],
["Threading / Parallelism", "IPC", "Threads and processes exchange data through IPC"],
["Threading / Parallelism", "RTOS Basics", "RTOS tasks, mutexes and priorities"],
["IPC", "RTOS Basics", "Queues and semaphores are the RTOS form of IPC"],
["RTOS Basics", "FreeRTOS", "The most common first RTOS"],
["RTOS Basics", "Zephyr", "The same scheduling, synchronisation and timing concepts"],
["RTOS Basics", "QNX", "The same scheduling, synchronisation and timing concepts"],
["RTOS Basics", "µC/OS", "The same scheduling, synchronisation and timing concepts"],
["RTOS Basics", "RT-Thread", "The same scheduling, synchronisation and timing concepts"],
["RTOS Basics", "Interrupts", "ISRs hand work over to tasks"],
["RTOS Basics", "Timers / Counters", "The tick timer and software timers"],
["FreeRTOS", "Memory Management", "FreeRTOS offers several heap allocation schemes"],
["FreeRTOS", "TCP/IP", "FreeRTOS+TCP is its TCP/IP stack"],
["Zephyr", "Bluetooth", "Zephyr ships its own Bluetooth stack"],
["Zephyr", "Thread", "Zephyr integrates OpenThread"],
["QNX", "IPC", "A microkernel built around message passing"],
["QNX", "Standards & Certifications", "Certified for safety-critical use"],
["GPIO", "Interrupts", "Pin edges raise interrupts"],
["GPIO", "PWM", "PWM is a timer-driven GPIO function"],
["GPIO", "Principles of Electric Circuits", "Pull-ups, drive strength and current limits"],
["GPIO", "Breadboarding", "The first experiment: an LED and a button on a breadboard"],
["ADC / DAC", "Sensors & Actuators", "Analog sensors are read through an ADC"],
["ADC / DAC", "DMA", "DMA moves samples without using the CPU"],
["ADC / DAC", "Digital Signal Processing", "Sampling theory and filtering"],
["ADC / DAC", "Timers / Counters", "Timers trigger conversions at a fixed rate"],
["Timers / Counters", "PWM", "PWM is generated by timer hardware"],
["Timers / Counters", "Interrupts", "Timer events raise interrupts"],
["Timers / Counters", "Clock Management", "Timer rates derive from the clock tree"],
["Timers / Counters", "Watchdog", "A watchdog is a special timer"],
["PWM", "Sensors & Actuators", "Drives motors, LEDs and servos"],
["PWM", "Control Theory", "A PID loop outputs a PWM duty cycle"],
["PWM", "Oscilloscope", "Duty cycle and frequency are checked on a scope"],
["Watchdog", "RTOS Basics", "Tasks must check in; a stuck task triggers a reset"],
["Interrupts", "Computer Architecture", "Vector table, priorities and the exception model"],
["DMA", "SPI", "High-throughput transfers without CPU load"],
["DMA", "UART", "Streaming serial data without CPU load"],
["Clock Management", "Power Management", "Clock gating and frequency scaling save power"],
["Clock Management", "UART", "Baud rates derive from the clock"],
["Clock Management", "Oscilloscope", "Checking oscillator and clock output"],
["Power Management", "Interrupts", "Wake-up sources are interrupts"],
["Power Management", "Bluetooth", "BLE is designed around duty cycling"],
["Power Management", "Hardware Design Basics", "Power supply and battery design"],
["Bootloader / DFU", "Memory Technologies & File Systems", "Firmware lives in flash and updates need safe writes"],
["Bootloader / DFU", "USB", "The USB DFU class updates firmware"],
["Memory Technologies & File Systems", "Memory Management", "RAM, flash and EEPROM sizes set the memory budget"],
["Memory Technologies & File Systems", "SPI", "SPI flash and SD cards"],
["Hardware Simulation / Emulation", "Digital Design", "Logic is verified in simulation before hardware exists"],
["Hardware Simulation / Emulation", "FPGA Development", "HDL designs are simulated before synthesis"],
["Hardware Simulation / Emulation", "Principles of Electric Circuits", "Circuits can be simulated (SPICE) before they are built"],
["Sensors & Actuators", "I2C", "Most digital sensors use I2C"],
["Sensors & Actuators", "SPI", "Faster sensors use SPI"],
["Sensors & Actuators", "Digital Signal Processing", "Raw readings are filtered and calibrated"],
["Sensors & Actuators", "Control Theory", "Closed-loop control reads sensors and drives actuators"],
["Digital Signal Processing", "Control Theory", "Filters and controllers share the same math"],
["Digital Signal Processing", "Basic Math & Calculus", "Transforms and filters are applied calculus"],
["Control Theory", "Basic Math & Calculus", "Differential equations and transfer functions"],
["UART", "Logic / Protocol Analyzer", "Inspect serial frames and baud rate"],
["UART", "USB", "USB-to-UART bridges are everywhere"],
["UART", "GSM / LTE", "Modems are controlled with AT commands over UART"],
["I2C", "Logic / Protocol Analyzer", "Decode addresses and ACK/NACK"],
["I2C", "SPI", "Both are on-board buses; learn when to choose which"],
["I2C", "Hardware Design Basics", "Pull-up sizing and bus capacitance"],
["SPI", "Logic / Protocol Analyzer", "Decode the clock, chip select and data lines"],
["Bluetooth", "Wi-Fi", "Both share the 2.4 GHz band, often on one radio chip"],
["Bluetooth", "Matter", "Matter devices are commissioned over Bluetooth LE"],
["Bluetooth", "UWB", "UWB is often paired with Bluetooth for secure ranging"],
["Wi-Fi", "TCP/IP", "Wi-Fi carries IP traffic"],
["Wi-Fi", "Matter", "Matter runs on Wi-Fi and Thread"],
["Thread", "Matter", "Matter runs on Wi-Fi and Thread"],
["Thread", "Zigbee", "Both are mesh networks on IEEE 802.15.4 radios"],
["LoRa", "NB-IoT", "Competing low-power wide-area options"],
["Ethernet", "TCP/IP", "Ethernet is the link layer under TCP/IP"],
["Ethernet", "EtherCAT", "Industrial protocols that run on Ethernet hardware"],
["Ethernet", "Profinet", "Industrial protocols that run on Ethernet hardware"],
["USB", "PCIe", "Both are high-speed serial links with host and device roles"],
["PCIe", "FPGA Development", "FPGAs often connect to hosts over PCIe"],
["Modbus", "UART", "Modbus RTU runs over serial lines such as RS-485"],
["Modbus", "TCP/IP", "Modbus TCP runs over TCP/IP"],
["EtherCAT", "FPGA Development", "EtherCAT slave controllers are often built in FPGAs"],
["GSM / LTE", "LTE-M / 5G", "The same cellular family, newer generations"],
["LTE-M / 5G", "NB-IoT", "Both are LTE-based low-power IoT standards"],
["MQTT", "TCP/IP", "MQTT runs over TCP"],
["MQTT", "CoAP", "Two IoT messaging styles: broker-based and request/response"],
["CoAP", "UDP", "CoAP runs over UDP"],
["TCP/IP", "UDP", "The two main transport protocols"],
["CAN", "LIN", "LIN is the low-cost sub-bus beneath CAN in vehicles"],
["CAN", "FlexRay", "FlexRay adds determinism and bandwidth beyond CAN"],
["CAN", "Logic / Protocol Analyzer", "Decode frames and arbitration"],
["CAN", "Oscilloscope", "Check the differential signal and bus termination"],
["MOST", "CAN", "MOST carries media in a vehicle while CAN carries control"],
["MOST", "Ethernet", "Automotive Ethernet is replacing MOST"],
["MIPI CSI-2", "MIPI DSI", "Camera and display links share the MIPI D-PHY physical layer"],
["MIPI CSI-2", "I2C", "Camera sensors are configured over a control bus such as I2C"],
["MIPI CSI-2", "Device Drivers", "Linux exposes camera sensors through V4L2 drivers"],
["MIPI CSI-2", "Edge AI", "Camera frames are the main input for on-device vision models"],
["MIPI DSI", "Device Drivers", "Linux drives DSI panels through the DRM/KMS subsystem"],
["MIPI DSI", "Embedded GUI", "The display link carries the interface the GUI renders"],
["MIPI DSI", "DMA", "Frame buffers are streamed to the display with DMA"],
["Basic Math & Calculus", "Principles of Electric Circuits", "Circuit analysis uses algebra and calculus"],
["Principles of Electric Circuits", "Electronics Fundamentals", "Components build on circuit laws"],
["Principles of Electric Circuits", "Multimeter", "Verify voltages and currents in real circuits"],
["Electronics Fundamentals", "Digital Design", "Logic gates are built from transistors"],
["Electronics Fundamentals", "Hardware Design Basics", "Choosing components and sizing circuits"],
["Electronics Fundamentals", "Oscilloscope", "Observe how signals change over time"],
["Digital Design", "Computer Architecture", "CPUs are built from digital logic"],
["Digital Design", "FPGA Development", "HDL describes digital logic"],
["Multimeter", "Breadboarding", "Continuity and voltage checks on prototypes"],
["Multimeter", "Soldering / Rework", "Checking for shorts after soldering"],
["Oscilloscope", "Logic / Protocol Analyzer", "Analog view and decoded digital view of the same signals"],
["Oscilloscope", "PCB Design / EMC", "Measuring noise and ringing"],
["Breadboarding", "Hardware Design Basics", "Prototype first, then design the board"],
["Breadboarding", "Soldering / Rework", "From breadboard to soldered prototype"],
["Hardware Design Basics", "PCB Design / EMC", "Schematics become board layout"],
["PCB Design / EMC", "Soldering / Rework", "Boards must allow assembly and repair"]
],
"groups": {
"Operating Systems": ["osBase", "linux", "rtos"],
"Microcontrollers": ["rtos", "mcu"],
"Interfaces & Protocols": ["basic", "wireless", "highSpeed", "industrial", "cellular", "network", "automotive", "display"]
},
"offMap": ["Don't Know Where to Start!", "Appendix-A: Advanced Topics"],
"readme": {
"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"],
"Display & Camera": ["Display & Camera 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"]
}
}
+639
View File
@@ -0,0 +1,639 @@
// 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 badge = { w: 103, h: 36 };
const logoSize = badge.h;
const iconY = fy + 9;
svg('image', { href: 'assets/logo.svg', x: fr - logoSize, y: iconY, width: logoSize, height: logoSize }, credits);
const badgeX = fr - logoSize - SPACE - badge.w;
svg('image', { href: 'assets/cc-by-sa.svg', x: badgeX, y: iconY, 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 bg = svg('rect', { class: 'header-bg', rx: 6, x: item.x - 6, y: item.y - 1, height: 24 }, g);
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, bg, x: item.x, 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', {}, box ? box.text : 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'),
box && box.related.length ? `${box.related.length} related` : null].filter(Boolean).join(' · ')),
h('div', { class: 'tip-hint' }, box && box.text !== topic.title ? `Click to open “${topic.title}”` : 'Click this topic to open its details'),
].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.reason ? 'curated' : 'evidence'}`,
'stroke-width': connection.reason ? 2.4 : 1.4,
}, 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);
}
}
// The overview of all connections only shows the hand-picked ones.
for (const connection of connections) if (connection.reason) 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.reason && !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';
}
const pageFitWidth = (size) => Math.max(page.w, (page.h + 40) / (size ? size.h / size.w : page.h / page.w)) + 40;
function fitPage() {
const size = canvasSize();
const width = pageFitWidth(size);
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;
// Limits only stop a step that goes further out of range; the whole-page view is always reachable.
const minScale = Math.min((size.w / page.w) * MIN_ZOOM, size.w / pageFitWidth(size), size.w / v.w);
if (factor > 1 ? scale > MAX_ZOOM : scale < minScale) 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 });
}
// Two fingers: zoom around their midpoint and pan with it.
const touches = new Map();
let pinch = null;
const spread = () => {
const [a, b] = [...touches.values()];
return { dist: Math.hypot(a.x - b.x, a.y - b.y), x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
};
function beginPinch() {
pinch = spread();
// The click that ends the gesture must not open a topic.
state.drag = { moved: true, id: null };
chart.classList.add('panning');
hover(null);
}
function movePinch() {
if (touches.size < 2) return;
const now = spread();
if (pinch.dist > 0 && now.dist > 0) zoomAt(now.dist / pinch.dist, now.x, now.y);
const unit = state.view.w / chart.getBoundingClientRect().width;
fitMode = 'custom';
setView({ ...state.view, x: state.view.x - (now.x - pinch.x) * unit, y: state.view.y - (now.y - pinch.y) * unit });
pinch = now;
}
function endTouch(e) {
if (!touches.delete(e.pointerId)) return;
if (pinch && touches.size < 2) {
pinch = null;
chart.classList.remove('panning');
}
if (!touches.size && state.drag && state.drag.id === null) window.setTimeout(() => { state.drag = null; }, 0);
}
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.pointerType === 'touch') touches.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (touches.size === 2) {
beginPinch();
return;
}
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) => {
if (touches.has(e.pointerId)) touches.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (pinch) {
movePinch();
return;
}
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) => {
endTouch(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);
// Safari on a Mac sends its own gesture events for trackpad pinches (other browsers send Ctrl + wheel).
// On iOS the same pinch also arrives as touches, which are handled above, so skip it there.
let gestureScale = 1;
chart.addEventListener('gesturestart', (e) => {
e.preventDefault();
gestureScale = 1;
});
chart.addEventListener('gesturechange', (e) => {
e.preventDefault();
if (touches.size) return;
zoomAt(e.scale / gestureScale, e.clientX, e.clientY);
gestureScale = e.scale;
});
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');
}
}
if (el.bg) el.bg.setAttribute('width', (el.text.getComputedTextLength() + 12).toFixed(1));
}
}
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)));
}
+102
View File
@@ -0,0 +1,102 @@
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 a "readme" entry in map.json)');
assert.deepEqual(plan.invalidReadme, [], 'map.json "readme" entries name a topic on the map and existing README headings');
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('every map topic is connected to related topics, each connection with a reason', () => {
const plan = linkDiagram(map, data);
assert.deepEqual(plan.invalidLinks, [], 'map.json "links" name existing topics, once, with a reason');
// Soft skills describe the engineer, not the technology, so some of them stand alone.
const alone = plan.boxes.filter((b) => !b.header && !b.areas.includes('SOFT SKILLS') && !b.related.length);
assert.deepEqual(alone.map((b) => b.text), [], 'topics without a connection');
const has = (a, b) => plan.connections.some((c) => c.reason && [c.a.text, c.b.text].sort().join('|') === [a, b].sort().join('|'));
assert.ok(has('C', 'Memory Management'));
assert.ok(has('Rust', 'Memory Management'));
});
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
+20
View File
@@ -0,0 +1,20 @@
// Warns (never fails) about README sections that have no place on the map yet.
// Usage: node site/tools/coverage.mjs Sections listed in map.json "offMap" (titles or group names) are skipped.
import { readFileSync } from 'node:fs';
import { parseRoadmap } from '../parser.js';
import { linkDiagram } from '../diagram.js';
const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8');
const map = JSON.parse(read('../map.json'));
const data = parseRoadmap(read('../../README.md'));
const plan = linkDiagram(map, data);
const drawn = new Set(plan.boxes.flatMap((b) => b.topics));
const onMap = (t) => drawn.has(t) || t.children.some(onMap);
const skipped = new Set(map.offMap || []);
const missing = data.root.children.filter((t) => !skipped.has(t.title) && !skipped.has(t.group) && !onMap(t));
for (const t of missing) {
console.log(`::warning file=README.md,title=Not on the roadmap map::"${t.title}" has no box on the map. Add it to site/map.json, or to "offMap" if it should stay off the map (see CONTRIBUTING.md).`);
}
console.log(missing.length ? `${missing.length} README section(s) are not on the map.` : 'Every README section is on the map or listed in "offMap".');
+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();
}