diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..5c914a2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,164 @@ +--- +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AlignArrayOfStructures: None +AlignConsecutiveMacros: None +AlignConsecutiveAssignments: None +AlignConsecutiveBitFields: None +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: Right +AlignOperands: Align +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortLambdasOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: true + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: true +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeComma +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 0 +CommentPragmas: "^ IWYU pragma:|^NOSONAR" +QualifierAlignment: Leave +CompactNamespaces: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DeriveLineEnding: true +DerivePointerAlignment: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +PackConstructorInitializers: Never +FixNamespaceComments: false +IncludeBlocks: Merge +IncludeCategories: + - Regex: ^(<|"(gtest|gmock)/) + Priority: 2 + - Regex: ^( and + +At this point, you're ready to make your changes! Feel free to ask for help; everyone is a beginner at first :smile_cat: + +If a maintainer asks you to "rebase" your PR, they're saying that a lot of code has changed and that you need to update your branch so it's easier to merge. diff --git a/Dockerfile.flex b/Dockerfile.flex new file mode 100644 index 0000000..9287315 --- /dev/null +++ b/Dockerfile.flex @@ -0,0 +1,20 @@ +FROM ghcr.io/philips-software/amp-devcontainer-cpp:5.1.4@sha256:46239906460dedb3baf3c33d9275f3de4f17d7a237fc136c2013b021589a6dbd AS builder + +HEALTHCHECK NONE + +#checkov:skip=CKV_DOCKER_3: this container needs to run as root + +WORKDIR /workspace +COPY . /workspace + +RUN cmake --preset flex \ + && cmake --build --preset flex + +FROM scratch + +WORKDIR /flex +COPY --from=builder /workspace/build/flex/postmaster/flex/postmaster.flex /flex/ +COPY --from=builder /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 + +ENTRYPOINT ["/flex/postmaster.flex"] +CMD ["--help"] diff --git a/Documents/Datasheets/Ethernet Connector.pdf b/Documents/Datasheets/Ethernet Connector.pdf new file mode 100644 index 0000000..b3fe028 Binary files /dev/null and b/Documents/Datasheets/Ethernet Connector.pdf differ diff --git a/Documents/Datasheets/LAN8710A.pdf b/Documents/Datasheets/LAN8710A.pdf new file mode 100644 index 0000000..f15b5ad Binary files /dev/null and b/Documents/Datasheets/LAN8710A.pdf differ diff --git a/Documents/Datasheets/STM32F407 Datasheet.pdf b/Documents/Datasheets/STM32F407 Datasheet.pdf new file mode 100644 index 0000000..0aca177 Binary files /dev/null and b/Documents/Datasheets/STM32F407 Datasheet.pdf differ diff --git a/Documents/Datasheets/STM32F407 Reference Manual.pdf b/Documents/Datasheets/STM32F407 Reference Manual.pdf new file mode 100644 index 0000000..8ed39b1 Binary files /dev/null and b/Documents/Datasheets/STM32F407 Reference Manual.pdf differ diff --git a/Documents/HardwareSoftwareInterface.md b/Documents/HardwareSoftwareInterface.md new file mode 100644 index 0000000..cbb4777 --- /dev/null +++ b/Documents/HardwareSoftwareInterface.md @@ -0,0 +1,69 @@ +# Postmaster Hardware Software Interface + +## Document Introduction +This document describes the interface between the hardware and the software of the Postmaster. It describes which peripherals and pins are connected to the hardware on the board. + +## Microcontroller + +The Postmaster software runs on an STM32F407VE. + +## Interface + +### STM32 Nucleo-F767ZI + +The Postmaster is connected to the STM32 Nucleo-F767ZI in this way: + +| Postmaster | STM32 Nucleo-F767ZI | +|----------------|---------------------| +| PB5 | Boot0 | +| PA10, UART1 RX | PC10 UART3/4 TX | +| PB6, UART1 TX | PC11 UART3/4 RX | +| PB7, UART1 RX | Extern Header TX | +| PA9, UART1 TX | Extern Header RX | +| PB4 | nReset | +| PC7, UART6 RX | PD8 UART3 TX | + +### Display + +The Display is a 128x32 pixels OLED displayed controlled by an SSD1306 chip. The address of this chip is 0x3c. This chip is connected over I2C (max. frequency 400kHz) in this way: + +| Postmaster | SSD1306 | +|---------------|---------| +| PB8, I2C1 SCL | SCL | +| PB9, I2C1 SDA | SDA | + +### Flash + +The Flash chip is a generic JEDEC serial NOR flash chip with at capacity of at least 2MB connected over SPI in this way: + +| Postmaster | Flash | +|-----------------|-------| +| PA15 | CS | +| PC10, SPI3 CLK | CLK | +| PC11, SPI3 MISO | MISO | +| PC12, SPI3 MOSI | MOSI | + +### Ethernet + +The Postmaster is connected to a LAN8710A Ethernet PHY chip via the SMI and RMII interfaces. Its SMI address is 0. + +| Postmaster | Ethernet | +|------------|------------| +| PA2 | mdio | +| PC1 | mdc | +| PA1 | rmiiRefClk | +| PA7 | rmiiCrsDv | +| PC4 | rmiiRxD0 | +| PC5 | rmiiRxD1 | +| PB11 | rmiiTxEn | +| PB12 | rmiiTxD0 | +| PB13 | rmiiTxD1 | + +### Debug UART + +The Debug UART is connected to the TDI and DBGRQ pins of the 20-pin J-Link Connector, which is presented over USB by the J-Link adapter as a virtual COM port. + +| Postmaster | Debug UART | +|---------------|---------------------------| +| PD8, UART3 TX | J-Link-RX (DBGRQ, pin 17) | +| PD9, UART3 RX | J-Link-TX (TDI, pin 5) | diff --git a/Documents/Requirements.md b/Documents/Requirements.md new file mode 100644 index 0000000..4782ff9 --- /dev/null +++ b/Documents/Requirements.md @@ -0,0 +1,41 @@ +# Postmaster Requirements Specification + +## Document Introduction +This document describes the product requirements and the functional behaviour of the Postmaster. + +## Purpose +The Postmaster is a tool intended to be used to download firmware to an attached STM32 Nucleo-F767ZI Board, and to connect to that board over a UART connection. The firmware to download is provided to Postmaster via its network connection. + +## Requirements +- The Postmaster attaches on top of an STM32 Nucleo-F767ZI board from ST Microelectronics via the Zio connector and the BOOT0 pin on the Morpho connector. +- The Postmaster draws its power from the Nucleo board to which it is attached. +- The Postmaster uses either the SWD or one of the boot loader protocols supported by the STM32F767 to upgrade the attached Nucleo-F767ZI +- The Postmaster connects to a TCP/IP network via an Ethernet connection. +- The Postmaster displays its current network status. +- The firmware of the Postmaster is remotely upgradeable. +- The Postmaster is programmable via a J-Link programmer. +- The Postmaster is discoverable via mDNS. + - The DNS hostname is .postmaster._tcp.local. + - Advertised attributes in the TXT record are: + - "nm", containing the Postmaster's configured name. + - "vs", containing the version, in format ... + - "vf", containing the full version, in format .. + - "at", containing the attributes describing the setup to which the Postmaster is attached to. +- The hostname is configurable; by default it contains information unique to the microcontroller of the Postmaster. +- Firmware for either the attached Nucleo-F767ZI or the Postmaster itself can be uploaded via the HTTP protocol. + - The path to upgrade the Postmaster is /firmware/self. + - The path to upgrade the attached Nucleo is /firmware/target. +- Several HTTP pages which accept POST requests are available, to distinguish between upgrading the Postmaster, upgrading the Nucleo-F767ZI, or patching (writing without erasing) the Nucleo-F767ZI. +- A user-friendly HTTP page is available for dragging/dropping firmware to be uploaded and for configuration. +- On booting, the Postmaster performs a self-test. +- An HTTP page is available that shows nearby detected Postmasters. +- The UART port with which the Postmaster communications with the attached Nucleo board can be configured to be either the same UART through which the Nucleo board is upgraded, or a secondary UART port. +- The HTTP connection can be upgraded to a WebSocket connection via which the Postmaster can talk to the attached Nucleo board over a UART port without interpretation of a protocol. + - The path for the attached Nucleo is /target/uart/programmer. + - The path for the external header is /target/uart/external. +- The HTTP connection can be upgraded to a WebSocket connection via which the Postmaster can talk to the attached Nucleo board over a UART port via the ECHO protocol. + - The path for the attached Nucleo is /target/echo/programmer. + - The path for the external header is /target/echo/external. + +## Assumptions +- The Postmaster is used in a secure network. The Postmaster therefore does not need access control or other security measurements. diff --git a/Documents/Review sheet Postmaster PCB.xlsm b/Documents/Review sheet Postmaster PCB.xlsm new file mode 100644 index 0000000..4e30d63 Binary files /dev/null and b/Documents/Review sheet Postmaster PCB.xlsm differ diff --git a/Documents/cd00264342-usart-protocol-used-in-the-stm32-bootloader-stmicroelectronics.pdf b/Documents/cd00264342-usart-protocol-used-in-the-stm32-bootloader-stmicroelectronics.pdf new file mode 100644 index 0000000..835ce32 Binary files /dev/null and b/Documents/cd00264342-usart-protocol-used-in-the-stm32-bootloader-stmicroelectronics.pdf differ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c057ab6 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,7 @@ +The MIT License (MIT) Copyright © [2018] Koninklijke Philips N.V, https://www.philips.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/PCB/Postmaster BOM.txt b/PCB/Postmaster BOM.txt new file mode 100644 index 0000000..fdf40ca --- /dev/null +++ b/PCB/Postmaster BOM.txt @@ -0,0 +1,61 @@ +Partlist exported from C:/Dev/Postmaster/PCB/Postmaster/Postmaster.sch at 17/04/2022 20:07:00 + +Part Value Device Package Description +B1 BEADSMD BEADSMD 0805 +C1 100n CAPACITOR0805 0805 +C2 100n CAPACITOR0805 0805 +C3 100n CAPACITOR0805 0805 +C4 100n CAPACITOR0805 0805 +C5 100n CAPACITOR0805 0805 +C6 100n CAPACITOR0805 0805 +C7 100n CAPACITOR0805 0805 +C8 100n CAPACITOR0805 0805 +C9 10p CAPACITOR0805 0805 +C10 10p CAPACITOR0805 0805 +C11 10p CAPACITOR0805 0805 +C12 10p CAPACITOR0805 0805 +C13 22n CAPACITOR0805 0805 +C14 470p CAPACITOR0805 0805 +C15 1u CAPACITOR0805 0805 +C16 100n CAPACITOR0805 0805 +C17 100n CAPACITOR0805 0805 +C18 100n CAPACITOR0805 0805 +C19 100n CAPACITOR0805 0805 +C20 100n CAPACITOR0805 0805 +C21 10u CAPACITOR0805 0805 +C22 10u CAPACITOR0805 0805 +C23 10u CAPACITOR0805 0805 +C24 10u CAPACITOR0805 0805 +C25 10u CAPACITOR0805 0805 +C26 2u2 CAPACITOR0805 0805 +C27 2u2 CAPACITOR0805 0805 +IC1 STM32F407 STM32F407 TQFP100 +IC2 S25FL116K S25FL116KW SOIC8-WIDE +IC3 DISPLAY_32_128_SSD1306 DISPLAY_32_128_SSD1306 DISPLAY_128_32_SSD1306 +IC4 LAN8710A LAN8710A QFN32 +IC5 X1G0044810012 SG7050CAN 50 MHZ SM77H SM77H 3.3V CMOS Clock Oscillator +Q1 8MHz RESONATOR-S RESONATOR-SMALL +R1 30 RESISTORR0805 0805 +R2 2k2 RESISTORR0805 0805 +R3 49.9 RESISTORR0805 0805 +R4 49.9 RESISTORR0805 0805 +R5 49.9 RESISTORR0805 0805 +R6 49.9 RESISTORR0805 0805 +R7 330 RESISTORR0805 0805 +R8 330 RESISTORR0805 0805 +R9 10k RESISTORR0805 0805 +R10 10k RESISTORR0805 0805 +R11 12k1 RESISTORR0805 0805 +R12 10k RESISTORR0805 0805 +R13 2k2 RESISTORR0805 0805 +R14 1k5 RESISTORR0805 0805 +R16 10k RESISTORR0805 0805 +R17 10k RESISTORR0805 0805 +R18 10k RESISTORR0805 0805 +R19 10k RESISTORR0805 0805 +R20 10k RESISTORR0805 0805 +S1 SWITCHSMD SWITCHSMD SWITCH-SMD +SJ1 SJ SJ SMD solder JUMPER +U$1 NUCLEO-TOPLEFT NUCLEO-TOPLEFT NUCLEO-TOPLEFT +X1 HEADER-IDC-10-2 HEADER-IDC-10-2 IDC-10_2 +X2 J00-0065NL J00-0065NL ETHERNET diff --git a/PCB/Postmaster/Postmaster.brd b/PCB/Postmaster/Postmaster.brd new file mode 100644 index 0000000..9603301 --- /dev/null +++ b/PCB/Postmaster/Postmaster.brd @@ -0,0 +1,2211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Postmaster 1.3 +TX +RX + + + + + +<b>PLASTIC 100-PIN TQFP</b> (PZ)<p> +Auto generated by <i>make-symbol-device-package-bsdl.ulp Rev. 19</i><br> +Source: http://focus.ti.com/docs/prod/folders/print/msp430fg4618.html + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<b>3.3V CMOS Clock Oscillator</b><p> +Source: www.pletronics.com .. sm77h%203.3v.pdf + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1uF 5.3 4.1<br> +4.7uF 5.3<br> +10uF 5.3 6.5<br> +22uF 5.3<br> +47uF 6.5<br> +100uF 6.5 8.2<br> +220uF 10.3<br> +330uF 8.3<br> +470uF 8.3<br> +680uF 10.2<br> +1000uF 10.2<br> +2200uF 16.2<br> + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + +<b>Jumpers</b><p> +<author>Created by librarian@cadsoft.de</author> + + +<b>Solder jumper</b> + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + +<b>JLCPCB design rules (2 layers)</b> +<ul> +<li>Board thickness: 1.6mm</li> +<li>Copper weight: 1oz (35um)</li> +<li>Note: annular ring aren't minimal</li> +</ul> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PCB/Postmaster/Postmaster.sch b/PCB/Postmaster/Postmaster.sch new file mode 100644 index 0000000..21aa554 --- /dev/null +++ b/PCB/Postmaster/Postmaster.sch @@ -0,0 +1,3669 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<b>PLASTIC 100-PIN TQFP</b> (PZ)<p> +Auto generated by <i>make-symbol-device-package-bsdl.ulp Rev. 19</i><br> +Source: http://focus.ti.com/docs/prod/folders/print/msp430fg4618.html + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<b>3.3V CMOS Clock Oscillator</b><p> +Source: www.pletronics.com .. sm77h%203.3v.pdf + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<b>3.3V CMOS Clock Oscillator</b><p> +Source: www.pletronics.com .. sm77h%203.3v.pdf + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Right LED +Left LED +1 TX+ +2 TX- +3 RX+ +6 RX- +4 +5 +7 +8 +>NAME +>VALUE +75R +75R +75R +75R +1 nF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1uF 5.3 4.1<br> +4.7uF 5.3<br> +10uF 5.3 6.5<br> +22uF 5.3<br> +47uF 6.5<br> +100uF 6.5 8.2<br> +220uF 10.3<br> +330uF 8.3<br> +470uF 8.3<br> +680uF 10.2<br> +1000uF 10.2<br> +2200uF 16.2<br> + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + +>NAME +>VALUE + + + + + + + +>NAME +>VALUE + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + +<b>OMRON SWITCH</b> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Source: http://search.murata.co.jp/Ceramy/CatalogshowpageAction.do?sDirnm=A07X&sFilnm=81G07006&sType=2&sLang=en&sNHinnm=CSTCR6M00G53Z-R0&sCapt=Standard_Land_Pattern_Dimensions + + + + + + + +>NAME +>VALUE + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + +>PART + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + +>NAME + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ++3.3V + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<b>Jumpers</b><p> +<author>Created by librarian@cadsoft.de</author> + + +<b>Solder jumper</b> + + + + + + + + + + + + + + +>NAME +>VALUE + + + +<b>Solder jumper</b> + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + + + + + + + + + + + + + + + + + + + +>NAME +>VALUE + + + + + + +SMD solder <b>JUMPER</b> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +J-Link +STM32407VE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PCB/Postmaster/eagle.epf b/PCB/Postmaster/eagle.epf new file mode 100644 index 0000000..adc7dcd --- /dev/null +++ b/PCB/Postmaster/eagle.epf @@ -0,0 +1,25 @@ +[Eagle] +Version="06 03 00" +Platform="Windows" +Serial="62191E841E-LSR-WLM-1EL" +Globals="Globals" +Desktop="Desktop" + +[Globals] +AutoSaveProject=1 +UsedLibrary="C:/Users/richa/Documents/eagle/lbr/Richard/diode.lbr" +UsedLibrary="C:/Users/richa/Documents/eagle/lbr/Richard/header.lbr" +UsedLibrary="C:/Users/richa/Documents/eagle/lbr/Richard/ic.lbr" +UsedLibrary="C:/Users/richa/Documents/eagle/lbr/Richard/passive.lbr" +UsedLibrary="C:/Users/richa/Documents/eagle/lbr/Richard/supply.lbr" +UsedLibrary="C:/Users/richa/Documents/eagle/lbr/Richard/transistor.lbr" + +[Win_1] +Type="Control Panel" +Loc="616 301 1215 700" +State=1 +Number=0 + +[Desktop] +Screen="1920 1080" +Window="Win_1" diff --git a/README.md b/README.md new file mode 100644 index 0000000..7dab706 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Postmaster (Programmer Of ST Microcontrollers Attached to ST Experiment boaRds) + + +[![Continuous Integration](https://github.com/philips-internal/amp-postmaster/workflows/Continuous%20Integration/badge.svg)](https://github.com/philips-internal/amp-postmaster/actions)[![Linting & Formatting](https://github.com/philips-internal/amp-postmaster/actions/workflows/linting-formatting.yml/badge.svg)](https://github.com/philips-internal/amp-postmaster/actions/workflows/linting-formatting.yml) + + +## Overview + +Postmaster is a device that clicks on top of an STM32 Nucleo-F767ZI board from ST Microelectronics, and through its Ethernet connector you can program the Nucleo and communicate with it. +In this way, Postmaster is usable in hardware-in-the-loop tests, where a test board is reachable through the internet. That test board can first be programmed with the correct firmware. For communicating with the test board, a client can connect to Postmaster via a websocket connection, and talk to the attached board either via a plain UART connection, or via an [ECHO](https://philips-software.github.io/amp-embedded-infra-lib/embedded_infrastructure_library/6.0.0/Echo.html) connection. This communication line is used to execute tests. +Test boards enabled by Postmaster are intended to be inherently scalable; Adding tests boards in a setup should be as simple as putting a Postmaster on top of it, configuring the Postmaster with a few labels describing the characteristics of that board, and connecting it to the internet. A locally installed proxy (also part of this archive) provides discoverability of all Postmasters in its vicinity, and routes incoming requests for boards with certain characteristics to any available board. +By providing a schematics and PCB layout, the Postmaster firmware, and various tools, this repository aims to provide a full suite of components tackling all aspects of connecting a hardware-in-the-loop test board to the cloud. + +Requirements of Postmaster are documented [here](Documents/Requirements.md). + +## Community + +This project uses the [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md) to define expected conduct in our community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project [CODEOWNER](.github/CODEOWNERS) + +## Changelog + +See [CHANGELOG](./CHANGELOG.md) for more info on what's been changed. + +## Build + +The Postmaster binaries can be built by executing the following commands: + +```shell +cmake --preset stm32f407 +cmake --build --preset stm32f407-MinSizeRel +``` + +The postmaster.flex utility can be build by executing the following commands: + +```shell +cmake --preset host +cmake --build --preset host-RelWithDebInfo +``` + +## Reporting vulnerabilities + +If you find a vulnerability, please report it to us! +See [security](.github/SECURITY.md) for more information. + +## Licenses + +See [LICENSE](./LICENSE.md) diff --git a/postmaster/CMakeLists.txt b/postmaster/CMakeLists.txt new file mode 100644 index 0000000..eaa92d3 --- /dev/null +++ b/postmaster/CMakeLists.txt @@ -0,0 +1,12 @@ +add_version_header_target(postmaster.version) + +add_subdirectory(bootloader) +add_subdirectory(discovery) +add_subdirectory(frontend) +add_subdirectory(flex) +add_subdirectory(instantiations_st) +add_subdirectory(instantiations) +add_subdirectory(postmaster_stm32f407) +add_subdirectory(programmer) +add_subdirectory(prototype_win) +add_subdirectory(upgrade_pack_builder) diff --git a/postmaster/bootloader/CMakeLists.txt b/postmaster/bootloader/CMakeLists.txt new file mode 100644 index 0000000..0bbc4c3 --- /dev/null +++ b/postmaster/bootloader/CMakeLists.txt @@ -0,0 +1,23 @@ +add_executable(postmaster.bootloader) + +# The bootloader does not fit in its flash section when building the Debug configuration. +emil_build_for(postmaster.bootloader TARGET_MCU_VENDOR st PREREQUISITE_CONFIG Release RelWithDebInfo MinSizeRel) + +target_sources(postmaster.bootloader PRIVATE + Main.cpp +) + +target_link_libraries(postmaster.bootloader PUBLIC + upgrade.boot_loader + services.util + hal_st.synchronous_stm32fxxx +) + +halst_target_linker_scripts(TARGET postmaster.bootloader LINKER_SCRIPTS + ${CMAKE_CURRENT_LIST_DIR}/mem_${TARGET_MCU}.ld + ${CMAKE_CURRENT_LIST_DIR}/sections.ld +) + +halst_target_default_init(postmaster.bootloader) + +emil_generate_artifacts(TARGET postmaster.bootloader BIN HEX MAP) diff --git a/postmaster/bootloader/Main.cpp b/postmaster/bootloader/Main.cpp new file mode 100644 index 0000000..a639fab --- /dev/null +++ b/postmaster/bootloader/Main.cpp @@ -0,0 +1,106 @@ +#include "generated/stm32fxxx/PinoutTableDefault.hpp" +#include "hal_st/cortex/InterruptCortex.hpp" +#include "hal_st/stm32fxxx/GpioStm.hpp" +#include "hal_st/synchronous_stm32fxxx/SynchronousFlashInternalStm.hpp" +#include "hal_st/synchronous_stm32fxxx/SynchronousSpiMasterStm.hpp" +#include "services/synchronous_util/SynchronousFlashRegion.hpp" +#include "services/synchronous_util/SynchronousFlashSpi.hpp" +#include "services/synchronous_util/SynchronousSpiMasterWithChipSelect.hpp" +#include "upgrade/boot_loader/DecryptorNone.hpp" +#include "upgrade/boot_loader/ImageUpgraderFlash.hpp" +#include "upgrade/boot_loader/PackUpgrader.hpp" +#include "upgrade/boot_loader/UpgradePackLoader.hpp" +#include "upgrade/boot_loader/VerifierHashOnly.hpp" + +// #define UPG_IN_INTERNAL_FLASH + +constexpr hal::SynchronousSpiMasterStm::Config CreateSpiConfig() +{ + hal::SynchronousSpiMasterStm::Config config; + config.msbFirst = true; + config.baudRatePrescaler = SPI_BAUDRATEPRESCALER_8; + return config; +} + +constexpr services::SynchronousFlashSpiConfig CreateFlashConfig() +{ + services::SynchronousFlashSpiConfig config; + config.numberOfSubSectors = 2048; + return config; +} + +void ProcessUpgradePack(infra::ConstByteRange flashMemory, uint32_t flashOffset) +{ + static hal::InterruptTable::WithStorage<128> interruptTable; + static hal::GpioStm gpio(hal::pinoutTableDefaultStm, hal::analogTableDefaultStm); + + static std::array sectorSizes{ + 0x08000, 0x08000, 0x08000, 0x08000, + 0x20000, 0x40000, 0x40000, 0x40000, + 0x40000, 0x40000, 0x40000, 0x40000 + }; + + hal::SynchronousFlashInternalStm internalFlash(infra::MakeRange(sectorSizes), flashMemory); + +#ifdef UPG_IN_INTERNAL_FLASH + services::SynchronousFlashRegion upgradePackFlash(internalFlash, 10, 2); +#else + hal::GpioPinStm spiChipSelect{ hal::Port::A, 15 }; + hal::GpioPinStm spiClock{ hal::Port::C, 10 }; + hal::GpioPinStm spiMiso{ hal::Port::C, 11 }; + hal::GpioPinStm spiMosi{ hal::Port::C, 12 }; + + hal::SynchronousSpiMasterStm spi(3, spiClock, spiMiso, spiMosi, CreateSpiConfig()); + services::SynchronousSpiMasterWithChipSelect spiWithChipSelect(spi, spiChipSelect); + services::SynchronousFlashSpi externalFlash(spiWithChipSelect, CreateFlashConfig()); + + services::SynchronousFlashRegion upgradePackFlash(externalFlash, 2, 510); +#endif + + application::DecryptorNone decryptor; + application::VerifierHashOnly verifier; + application::UpgradePackLoader loader(upgradePackFlash, "Postmaster"); + + if (loader.Load(decryptor, verifier)) + { + application::ImageUpgraderFlash::WithBlockSize<4096> upgraderInternalFlash("app", decryptor, internalFlash, flashOffset); + std::array imageUpgraders{ &upgraderInternalFlash }; + application::PackUpgrader packUpgrader(upgradePackFlash); + + packUpgrader.UpgradeFromImages(imageUpgraders); + } +} + +[[noreturn]] void Jump(const void* isr) +{ + uint8_t* stackAddress = reinterpret_cast(isr)[0]; + uint8_t* jumpAddress = reinterpret_cast(isr)[1]; + + for (std::size_t i = 0; i < 7; ++i) + { + NVIC->ICER[i] = 0xFFFFFFFF; + NVIC->ICPR[i] = 0xFFFFFFFF; + } + + __set_MSP(reinterpret_cast(stackAddress)); + reinterpret_cast(jumpAddress)(); + + __builtin_unreachable(); +} + +unsigned int hse_value = 8000000; + +int main() +{ + HAL_Init(); + + extern uint8_t _flash_start; + extern uint8_t _flash_end; + infra::ConstByteRange flashMemory(&_flash_start, &_flash_end); + ProcessUpgradePack(flashMemory, ~(reinterpret_cast(&_flash_start)) + 1); + + extern uint8_t _application_start; + extern uint8_t _application_end; + infra::ConstByteRange applicationMemory(&_application_start, &_application_end); + Jump(applicationMemory.begin()); +} diff --git a/postmaster/bootloader/mem_stm32f407.ld b/postmaster/bootloader/mem_stm32f407.ld new file mode 100644 index 0000000..fb6b2eb --- /dev/null +++ b/postmaster/bootloader/mem_stm32f407.ld @@ -0,0 +1,12 @@ +MEMORY +{ + RAM (w) : ORIGIN = 0x20000000, LENGTH = 128K + RAM_SHARED (xrw) : ORIGIN = 0, LENGTH = 0 + BOOTLOADER (rx) : ORIGIN = 0x08000000, LENGTH = 32K + APPLICATION (rx) : ORIGIN = 0x08008000, LENGTH = 512K - 32K +} + +_flash_start = ORIGIN(BOOTLOADER); +_flash_end = ORIGIN(APPLICATION) + LENGTH(APPLICATION); +_application_start = ORIGIN(APPLICATION); +_application_end = ORIGIN(APPLICATION) + LENGTH(APPLICATION); diff --git a/postmaster/bootloader/mem_stm32f767.ld b/postmaster/bootloader/mem_stm32f767.ld new file mode 100644 index 0000000..c0da125 --- /dev/null +++ b/postmaster/bootloader/mem_stm32f767.ld @@ -0,0 +1,14 @@ +MEMORY +{ + RAM (w) : ORIGIN = 0x20020000, LENGTH = 368K + RAM_SHARED (xrw) : ORIGIN = 0, LENGTH = 0 + DTCM_RAM : ORIGIN = 0x20000000, LENGTH = 128K + ITCM_RAM : ORIGIN = 0x00000000, LENGTH = 16K + BOOTLOADER (rx) : ORIGIN = 0x08000000, LENGTH = 32K + APPLICATION (rx) : ORIGIN = 0x08008000, LENGTH = 2048K - 32K +} + +_flash_start = ORIGIN(BOOTLOADER); +_flash_end = ORIGIN(APPLICATION) + LENGTH(APPLICATION); +_application_start = ORIGIN(APPLICATION); +_application_end = ORIGIN(APPLICATION) + LENGTH(APPLICATION); diff --git a/postmaster/bootloader/sections.ld b/postmaster/bootloader/sections.ld new file mode 100644 index 0000000..fde09bf --- /dev/null +++ b/postmaster/bootloader/sections.ld @@ -0,0 +1,170 @@ +__stack = ORIGIN(RAM) + LENGTH(RAM); +_estack = __stack; /* STM specific definition */ + +__Main_Stack_Size = 1024 ; +PROVIDE(_Main_Stack_Size = __Main_Stack_Size); + +__Main_Stack_Limit = __stack - __Main_Stack_Size ; +PROVIDE (_Main_Stack_Limit = __Main_Stack_Limit); + +PROVIDE(_Heap_Begin = __end__); +PROVIDE(_Heap_Limit = __stack - __Main_Stack_Size); + +EXTERN(__isr_vectors) + +ENTRY(Reset_Handler) + +SECTIONS +{ + .isr_vector : ALIGN(4) + { + FILL(0xFF) + + KEEP(*(.isr_vector)) + *(.after_vectors .after_vectors.*) + } >BOOTLOADER + + .inits : ALIGN(4) + { + /* + * These are the old initialisation sections, intended to contain + * naked code, with the prologue/epilogue added by crti.o/crtn.o + * when linking with startup files. The standalone startup code + * currently does not run these, better use the init arrays below. + */ + KEEP(*(.init)) + KEEP(*(.fini)) + _fini = .; + + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP(*(.preinit_array_sysinit .preinit_array_sysinit.*)) + KEEP(*(.preinit_array_platform .preinit_array_platform.*)) + KEEP(*(.preinit_array .preinit_array.*)) + PROVIDE_HIDDEN (__preinit_array_end = .); + + . = ALIGN(4); + PROVIDE_HIDDEN (__init_array_start = .); + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array)) + PROVIDE_HIDDEN (__init_array_end = .); + + . = ALIGN(4); + PROVIDE_HIDDEN (__fini_array_start = .); + KEEP(*(SORT(.fini_array.*))) + KEEP(*(.fini_array)) + PROVIDE_HIDDEN (__fini_array_end = .); + } >BOOTLOADER + + .text : ALIGN(4) + { + *(.text .text.*) + + KEEP(*(.eh_frame*)) + + *(.glue_7) /* Linker-generated stubs for ARM code calling Thumb code */ + *(.glue_7t) /* Linker-generated stubs for Thumb code calling ARM code */ + + . = ALIGN(4); + _etext = .; + } >BOOTLOADER + + .rodata : ALIGN(4) + { + *(.rodata) + *(.rodata*) + } >BOOTLOADER + + /* Exception sections */ + .ARM.extab : ALIGN(4) + { + *(.ARM.extab* .gnu.linkonce.armextab.*) + } > BOOTLOADER + + .ARM.exidx : ALIGN(4) + { + __exidx_start = .; + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + . = ALIGN(4); + __exidx_end = .; + } > BOOTLOADER + + _sidata = LOADADDR(.data); + + .data : ALIGN(4) + { + FILL(0xFF) + _sdata = .; + __data_start__ = _sdata; + *(.data .data.*) + . = ALIGN(4); + _edata = .; + __data_end__ = _edata; + + } >RAM AT>BOOTLOADER + + .bss (NOLOAD) : ALIGN(4) + { + _sbss = .; + __bss_start__ = _sbss; + *(.bss .bss.*) + *(COMMON) + . = ALIGN(4); + _ebss = .; + __bss_end__ = _ebss; + } >RAM + + .noinit (NOLOAD) : ALIGN(4) + { + _noinit = .; + *(.noinit .noinit.*) + . = ALIGN(4) ; + _end_noinit = .; + } >RAM + + PROVIDE(__end__ = _end_noinit); + + MAPPING_TABLE (NOLOAD) : { *(MAPPING_TABLE) } >RAM_SHARED + MB_MEM1 (NOLOAD) : { *(MB_MEM1) } >RAM_SHARED + MB_MEM2 (NOLOAD) : { _sMB_MEM2 = . ; *(MB_MEM2) ; _eMB_MEM2 = . ; } >RAM_SHARED + + ._check_stack : ALIGN(4) + { + . = . + __Main_Stack_Size ; + } >RAM + + /* Stabs debugging sections. */ + .stab 0 : { *(.stab) } + .stabstr 0 : { *(.stabstr) } + .stab.excl 0 : { *(.stab.excl) } + .stab.exclstr 0 : { *(.stab.exclstr) } + .stab.index 0 : { *(.stab.index) } + .stab.indexstr 0 : { *(.stab.indexstr) } + .comment 0 : { *(.comment) } + + /* DWARF debug sections. + * Symbols in the DWARF debugging sections are relative to the beginning + * of the section so we begin them at 0. + */ + /* DWARF 1 */ + .debug 0 : { *(.debug) } + .line 0 : { *(.line) } + /* GNU DWARF 1 extensions */ + .debug_srcinfo 0 : { *(.debug_srcinfo) } + .debug_sfnames 0 : { *(.debug_sfnames) } + /* DWARF 1.1 and DWARF 2 */ + .debug_aranges 0 : { *(.debug_aranges) } + .debug_pubnames 0 : { *(.debug_pubnames) } + /* DWARF 2 */ + .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_macinfo 0 : { *(.debug_macinfo) } + /* SGI/MIPS DWARF 2 extensions */ + .debug_weaknames 0 : { *(.debug_weaknames) } + .debug_funcnames 0 : { *(.debug_funcnames) } + .debug_typenames 0 : { *(.debug_typenames) } + .debug_varnames 0 : { *(.debug_varnames) } +} diff --git a/postmaster/discovery/CMakeLists.txt b/postmaster/discovery/CMakeLists.txt new file mode 100644 index 0000000..2c54253 --- /dev/null +++ b/postmaster/discovery/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(postmaster.discovery) +emil_build_for(postmaster.discovery HOST All) + +target_sources(postmaster.discovery PRIVATE + Main.cpp +) + +target_link_libraries(postmaster.discovery PRIVATE + hal.generic + postmaster.instantiations + services.network_instantiations +) diff --git a/postmaster/discovery/Main.cpp b/postmaster/discovery/Main.cpp new file mode 100644 index 0000000..5eee68a --- /dev/null +++ b/postmaster/discovery/Main.cpp @@ -0,0 +1,186 @@ +// Start Traefik with: docker run -it --rm -p 80:80 -p 8080:8080 traefik:v2.10 --api.insecure=true --providers.http.endpoint=http://host.docker.internal:9000/api --log.level=DEBUG + +#include "hal/generic/TimerServiceGeneric.hpp" +#include "infra/stream/StdStringOutputStream.hpp" +#include "infra/syntax/JsonFormatter.hpp" +#include "postmaster/instantiations/MdnsDiscovery.hpp" +#include "services/network/Http.hpp" +#include "services/network/HttpErrors.hpp" +#include "services/network/HttpServer.hpp" +#include "services/network_instantiations/NetworkAdapter.hpp" +#include "services/tracer/GlobalTracer.hpp" + +class HttpPageReverseProxyConfiguration + : public services::SimpleHttpPage + , protected services::HttpResponse +{ +public: + HttpPageReverseProxyConfiguration(infra::BoundedConstString path, application::PostmasterDiscovery& discovery); + + // Implementation of SimpleHttpPage + bool ServesRequest(const infra::Tokenizer& pathTokens) const override; + void RespondToRequest(services::HttpRequestParser& parser, services::HttpServerConnection& connection) override; + +protected: + // Implementation of HttpResponse + infra::BoundedConstString Status() const override; + void WriteBody(infra::TextOutputStream& stream) const override; + infra::BoundedConstString ContentType() const override; + +private: + infra::BoundedConstString path; + application::PostmasterDiscovery& discovery; + mutable std::string json; +}; + +HttpPageReverseProxyConfiguration::HttpPageReverseProxyConfiguration(infra::BoundedConstString path, application::PostmasterDiscovery& discovery) + : path(path) + , discovery(discovery) +{} + +bool HttpPageReverseProxyConfiguration::ServesRequest(const infra::Tokenizer& pathTokens) const +{ + return pathTokens.TokenAndRest(0) == path; +} + +void HttpPageReverseProxyConfiguration::RespondToRequest(services::HttpRequestParser& parser, services::HttpServerConnection& connection) +{ + if (parser.Verb() == services::HttpVerb::get) + connection.SendResponse(*this); + else + connection.SendResponse(services::HttpResponseMethodNotAllowed::Instance()); +} + +infra::BoundedConstString HttpPageReverseProxyConfiguration::Status() const +{ + return services::http_responses::ok; +} + +void HttpPageReverseProxyConfiguration::WriteBody(infra::TextOutputStream& stream) const +{ + struct Group + { + std::string groupName; + infra::BoundedConstString attributes; + std::vector urls; + }; + + std::vector groups; + + int index = 0; + discovery.EnumerateDiscoveredPostmasters([&groups, &index](const application::PostmasterInfo& postmaster, infra::Duration) + { + services::GlobalTracer().Trace() << "Discovered postmaster: " << postmaster.hostname << " with attributes " << postmaster.attributes; + + for (auto& group : groups) + if (group.attributes == postmaster.attributes) + { + infra::StdStringOutputStream::WithStorage url; + url << "http://" << postmaster.ipAddress << "/"; + group.urls.emplace_back(url.Storage()); + return; + } + + infra::StdStringOutputStream::WithStorage serviceName; + serviceName << "group" << index; + infra::StdStringOutputStream::WithStorage url; + url << "http://" << postmaster.ipAddress << "/"; + groups.emplace_back(Group{ serviceName.Storage(), postmaster.attributes, { "http://host.docker.internal:90", url.Storage() } }); + ++index; + }); + + infra::StdStringOutputStream::WithStorage routersFormatterStream; + { + infra::JsonObjectFormatter routersFormatter{ routersFormatterStream }; + for (const auto& group : groups) + { + auto groupFormatterStream{ routersFormatter.AddObject(group.groupName.c_str()) }; + infra::JsonObjectFormatter groupFormatter{ groupFormatterStream }; + { + auto entryPoints(groupFormatter.SubArray("entrypoints")); + entryPoints.Add("http"); + } + groupFormatter.Add("service", group.groupName); + infra::StdStringOutputStream::WithStorage rule; + rule << "PathPrefix(`/" << group.attributes << "`)"; + groupFormatter.Add("rule", rule.Storage()); + { + auto middlewaresFormatter{ groupFormatter.SubArray("middlewares") }; + middlewaresFormatter.Add(group.groupName); + } + } + } + + infra::StdStringOutputStream::WithStorage middlewaresFormatterStream; + { + infra::JsonObjectFormatter middlewaresFormatter(middlewaresFormatterStream); + for (const auto& group : groups) + { + auto redirectionFormatterStream{ middlewaresFormatter.AddObject(group.groupName.c_str()) }; + infra::JsonObjectFormatter redirectionFormatter{ redirectionFormatterStream }; + auto groupFormatterStream{ redirectionFormatter.AddObject("stripPrefix") }; + infra::JsonObjectFormatter groupFormatter{ groupFormatterStream }; + infra::StdStringOutputStream::WithStorage prefix; + prefix << "/" + group.attributes; + groupFormatter.Add("prefixes", prefix.Storage()); + } + } + + infra::StdStringOutputStream::WithStorage servicesFormatterStream; + { + infra::JsonObjectFormatter servicesFormatter(servicesFormatterStream); + + for (const auto& group : groups) + { + auto pageServerFormatterStream{ servicesFormatter.AddObject(group.groupName.c_str()) }; + infra::JsonObjectFormatter pageServerFormatter{ pageServerFormatterStream }; + auto loadBalancerFormatterStream{ pageServerFormatter.AddObject("loadBalancer") }; + infra::JsonObjectFormatter loadBalancerFormatter{ loadBalancerFormatterStream }; + infra::JsonArrayFormatter serversFormatter{ loadBalancerFormatter.SubArray("servers") }; + for (const auto& url : group.urls) + { + infra::JsonObjectFormatter serverFormatter{ serversFormatter.SubObject() }; + serverFormatter.Add("url", url); + } + } + } + + infra::StdStringOutputStream::WithStorage resultFormatterStream; + { + infra::JsonObjectFormatter topFormatter(resultFormatterStream); + auto httpFormatterStream{ topFormatter.AddObject("http") }; + infra::JsonObjectFormatter httpFormatter{ httpFormatterStream }; + httpFormatter.AddSubObject("routers", routersFormatterStream.Storage()); + httpFormatter.AddSubObject("middlewares", middlewaresFormatterStream.Storage()); + httpFormatter.AddSubObject("services", servicesFormatterStream.Storage()); + } + + services::GlobalTracer().Trace() << "Output: " << resultFormatterStream.Storage(); + + stream << resultFormatterStream.Storage(); +} + +infra::BoundedConstString HttpPageReverseProxyConfiguration::ContentType() const +{ + return "text/json"; +} + +int main(int argc, const char* argv[], const char* env[]) +{ + static hal::TimerServiceGeneric timerService; + static main_::NetworkAdapter network; + static services::DefaultHttpServer::WithBuffer<2048> httpServer(network.ConnectionFactory(), 90); + static services::HttpPageWithContent page("page", "

Welcome

", "text/html"); + + static main_::MdnsDiscovery discovery{ network.DatagramFactory(), network.Multicast() }; + + static services::DefaultHttpServer::WithBuffer<2048> traefikConfigurationServer(network.ConnectionFactory(), 9000); + static HttpPageReverseProxyConfiguration configuration{ "api", discovery.discovery }; + + httpServer.AddPage(page); + traefikConfigurationServer.AddPage(configuration); + + network.Run(); + + return 0; +} diff --git a/postmaster/flex/CMakeLists.txt b/postmaster/flex/CMakeLists.txt new file mode 100644 index 0000000..ab7ed15 --- /dev/null +++ b/postmaster/flex/CMakeLists.txt @@ -0,0 +1,23 @@ +add_executable(postmaster.flex) +emil_build_for(postmaster.flex HOST All) + +target_sources(postmaster.flex PRIVATE + EchoWebSocketClientFactory.cpp + EchoWebSocketClientFactory.hpp + FlexHttpClient.cpp + FlexHttpClient.hpp + HttpClientAuthenticationDigest.cpp + HttpClientAuthenticationDigest.hpp + Main.cpp +) + +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_link_options(postmaster.flex PRIVATE -static -static-libgcc -static-libstdc++) +endif() + +target_link_libraries(postmaster.flex PRIVATE + args + services.network_instantiations + hal.generic + postmaster.frontend +) diff --git a/postmaster/flex/EchoWebSocketClientFactory.cpp b/postmaster/flex/EchoWebSocketClientFactory.cpp new file mode 100644 index 0000000..17b1c23 --- /dev/null +++ b/postmaster/flex/EchoWebSocketClientFactory.cpp @@ -0,0 +1,52 @@ +#include "postmaster/flex/EchoWebSocketClientFactory.hpp" + +namespace application +{ + EchoWebSocketClientFactory::EchoWebSocketClientFactory(infra::BoundedString url, uint16_t port, services::ConnectionFactory& connectionFactory, services::Tracer& tracer) + : url(url) + , port(port) + , connectionFactory(connectionFactory) + , tracer(tracer) + {} + + infra::BoundedString EchoWebSocketClientFactory::Url() const + { + infra::BoundedString result(url); + result += "/target/echo/programmer"; + return result; + } + + uint16_t EchoWebSocketClientFactory::Port() const + { + return port; + } + + void EchoWebSocketClientFactory::ConnectionEstablished(infra::AutoResetFunction client)>&& createdClientObserver) + { + // This connection is not created directly but taken over from an existing HTTP connection + std::abort(); + } + + void EchoWebSocketClientFactory::ConnectionFailed(ConnectFailReason reason) + { + // This connection is not created directly but taken over from an existing HTTP connection + std::abort(); + } + + void EchoWebSocketClientFactory::WebSocketInitiationDone(services::Connection& connection) + { + tracer.Trace() << "WebSocketInitiationDone"; + + auto webSocketConnection = webSocket.Emplace(); + connection.Detach(); + connection.Attach(webSocketConnection); + webSocketConnection->Attach(echo.Emplace(serializerFactoryWebSocket)); + + listener.Emplace(connectionFactory, 1234, services::SingleConnectionListener::Creators{ connectionCreator }); + } + + void EchoWebSocketClientFactory::WebSocketInitiationError(WebSocketClientObserverFactory::ConnectFailReason reason) + { + tracer.Trace() << "WebSocketInitiationError"; + } +} diff --git a/postmaster/flex/EchoWebSocketClientFactory.hpp b/postmaster/flex/EchoWebSocketClientFactory.hpp new file mode 100644 index 0000000..e7ce7b9 --- /dev/null +++ b/postmaster/flex/EchoWebSocketClientFactory.hpp @@ -0,0 +1,61 @@ +#ifndef POSTMASTER_ECHO_WEB_SOCKET_CLIENT_FACTORY_HPP +#define POSTMASTER_ECHO_WEB_SOCKET_CLIENT_FACTORY_HPP + +#include "protobuf/echo/ServiceForwarder.hpp" +#include "services/network/EchoOnConnection.hpp" +#include "services/network/SingleConnectionListener.hpp" +#include "services/network/WebSocketClientConnectionObserver.hpp" +#include "services/tracer/Tracer.hpp" + +namespace application +{ + class EchoWebSocketClientFactory + : public services::WebSocketClientObserverFactory + , public services::HttpClientWebSocketInitiationResult + { + public: + EchoWebSocketClientFactory(infra::BoundedString url, uint16_t port, services::ConnectionFactory& connectionFactory, services::Tracer& tracer); + + // Implementation of WebSocketClientObserverFactory + infra::BoundedString Url() const override; + uint16_t Port() const override; + void ConnectionEstablished(infra::AutoResetFunction client)>&& createdClientObserver) override; + void ConnectionFailed(ConnectFailReason reason) override; + + // Implementation of HttpClientWebSocketInitiationResult + void WebSocketInitiationDone(services::Connection& connection); + void WebSocketInitiationError(WebSocketClientObserverFactory::ConnectFailReason reason); + + private: + infra::BoundedString url; + uint16_t port; + services::ConnectionFactory& connectionFactory; + services::Tracer& tracer; + + services::MethodSerializerFactory::OnHeap serializerFactoryWebSocket; + services::MethodSerializerFactory::OnHeap serializerFactoryListener; + infra::SharedOptional webSocket; + infra::SharedOptional echo; + infra::Optional listener; + infra::Optional listenConnection; + + infra::CreatorExternal connectionCreator{ [this](services::IPAddress address) -> services::ConnectionObserver& + { + listenConnection.Emplace(serializerFactoryListener); + forwarder1.Emplace(*echo, *listenConnection); + forwarder2.Emplace(*listenConnection, *echo); + return *listenConnection; + }, + [this]() + { + forwarder1 = infra::none; + forwarder2 = infra::none; + listenConnection = infra::none; + } }; + + infra::Optional forwarder1; + infra::Optional forwarder2; + }; +} + +#endif diff --git a/postmaster/flex/FlexHttpClient.cpp b/postmaster/flex/FlexHttpClient.cpp new file mode 100644 index 0000000..ed3b503 --- /dev/null +++ b/postmaster/flex/FlexHttpClient.cpp @@ -0,0 +1,73 @@ +#include "postmaster/flex/FlexHttpClient.hpp" + +namespace application +{ + FlexHttpClient::FlexHttpClient(infra::BoundedString url, uint16_t port, services::HttpClientConnector& connector, const std::vector& firmware, services::HttpClientWebSocketInitiation& webSocketInitiation, services::Tracer& tracer) + : HttpClientBasic(url, port, connector, std::chrono::minutes(10), services::noAutoConnect) + , firmware(firmware) + , webSocketInitiation(webSocketInitiation) + , tracer(tracer) + { + if (!firmware.empty()) + Connect(); + else + connector.Connect(webSocketInitiation); + } + + void FlexHttpClient::Attached() + { + HttpClientBasic::Attached(); + + tracer.Trace() << "Attached"; + infra::BoundedString path = Path(); + path += "/firmware/target"; + + httpClient = &Subject(); + httpClient->Post(path, infra::ByteRangeAsString(infra::MakeRange(firmware)), Headers()); + } + + void FlexHttpClient::Detaching() + { + detached = true; + } + + void FlexHttpClient::StatusAvailable(services::HttpStatusCode statusCode) + { + HttpClientBasic::StatusAvailable(statusCode); + + tracer.Trace() << "Status: " << statusCode; + } + + void FlexHttpClient::HeaderAvailable(services::HttpHeader header) + { + tracer.Trace() << "Header: " << header; + } + + void FlexHttpClient::BodyAvailable(infra::SharedPtr&& reader) + { + infra::DataInputStream::WithErrorPolicy stream(*reader, infra::noFail); + + while (!stream.Empty()) + tracer.Trace() << infra::ByteRangeAsString(stream.ContiguousRange()); + } + + void FlexHttpClient::Done() + { + tracer.Trace() << "Done"; + + httpClient->Attach(infra::UnOwnedSharedPtr(webSocketInitiation)); + } + + void FlexHttpClient::Error(bool intermittentFailure) + { + tracer.Trace() << "Error"; + + if (!detached && httpClient != nullptr) + httpClient->CloseConnection(); + } + + void FlexHttpClient::CloseConnection() + { + Subject().Detach(); + } +} diff --git a/postmaster/flex/FlexHttpClient.hpp b/postmaster/flex/FlexHttpClient.hpp new file mode 100644 index 0000000..895db08 --- /dev/null +++ b/postmaster/flex/FlexHttpClient.hpp @@ -0,0 +1,38 @@ +#ifndef POSTMASTER_FLEX_HTTP_CLIENT_HPP +#define POSTMASTER_FLEX_HTTP_CLIENT_HPP + +#include "services/network/HttpClientBasic.hpp" +#include "services/network/WebSocketClientConnectionObserver.hpp" +#include "services/tracer/Tracer.hpp" +#include + +namespace application +{ + class FlexHttpClient + : private services::HttpClientBasic + { + public: + FlexHttpClient(infra::BoundedString url, uint16_t port, services::HttpClientConnector& connector, const std::vector& firmware, services::HttpClientWebSocketInitiation& webSocketInitiation, services::Tracer& tracer); + + protected: + // Implementation of HttpClientBasic + void Attached() override; + void Detaching() override; + void StatusAvailable(services::HttpStatusCode statusCode) override; + void HeaderAvailable(services::HttpHeader header) override; + void BodyAvailable(infra::SharedPtr&& reader); + void Done() override; + void Error(bool intermittentFailure) override; + void CloseConnection() override; + + private: + std::vector firmware; + services::HttpClientWebSocketInitiation& webSocketInitiation; + services::Tracer& tracer; + + services::HttpClient* httpClient = nullptr; + bool detached = false; + }; +} + +#endif diff --git a/postmaster/flex/HttpClientAuthenticationDigest.cpp b/postmaster/flex/HttpClientAuthenticationDigest.cpp new file mode 100644 index 0000000..bfc2e00 --- /dev/null +++ b/postmaster/flex/HttpClientAuthenticationDigest.cpp @@ -0,0 +1,114 @@ +#include "postmaster/flex/HttpClientAuthenticationDigest.hpp" +#include "infra/stream/StringOutputStream.hpp" +#include "postmaster/frontend/AuthenticatedHttpPage.hpp" +#include "postmaster/frontend/CommaSeparatedKeyValueListParser.hpp" + +namespace application +{ + HttpClientAuthenticationDigest::HttpClientAuthenticationDigest(infra::BoundedVector& headersWithAuthorization, infra::BoundedConstString password, hal::SynchronousRandomDataGenerator& randomDataGenerator) + : HttpClientAuthentication(headersWithAuthorization) + , password(password) + , randomDataGenerator(randomDataGenerator) + {} + + void HttpClientAuthenticationDigest::Authenticate(services::HttpVerb verb, infra::BoundedConstString target, infra::BoundedConstString scheme, infra::BoundedConstString value) + { + if (scheme == "Digest") + ComputeAuthenticationResponse(verb, target, value); + } + + infra::BoundedConstString HttpClientAuthenticationDigest::AuthenticationHeader() const + { + return authenticationResponse; + } + + bool HttpClientAuthenticationDigest::Retry() const + { + return retry; + } + + void HttpClientAuthenticationDigest::Reset() + { + retry = false; + } + + void Stream(infra::TextOutputStream& stream, infra::BoundedConstString tag, infra::BoundedConstString value, bool quoted) + { + stream << tag << "="; + + if (quoted) + stream << '"'; + + stream << value; + + if (quoted) + stream << '"'; + + stream << ','; + } + + void HttpClientAuthenticationDigest::ComputeAuthenticationResponse(services::HttpVerb verb, infra::BoundedConstString target, infra::BoundedConstString keyValues) + { + Authentication::DigestData digestData; + CommaSeparatedKeyValueListParser keyValueParser(keyValues, [&digestData](infra::BoundedConstString key, infra::BoundedConstString value) + { + if (key == "realm") + digestData.realm = value; + if (key == "nonce") + digestData.nonce = value; + if (key == "qop") + digestData.qop = value; + }); + + if (keyValueParser.Valid()) + { + digestData.username = infra::BoundedConstString("username"); + digestData.uri = target; + ncString.clear(); + infra::StringOutputStream ncStream(ncString); + ncStream << infra::hex << infra::Width(8, '0') << nc; + digestData.nc = ncString; + std::array cnonce; + randomDataGenerator.GenerateRandomData(cnonce); + cnonceString.clear(); + infra::StringOutputStream cnonceStream(cnonceString); + cnonceStream << infra::AsHex(cnonce); + digestData.cnonce = cnonceString; + + infra::StringOutputStream::WithStorage<256> input; + input << digestData.username << ":postmaster:" << password; + auto hash1 = sha.Calculate(infra::StringAsByteRange(input.Storage())); + + input.Storage().clear(); + input << services::HttpVerbToString(verb) << ":" << digestData.uri; + auto hash2 = sha.Calculate(infra::StringAsByteRange(input.Storage())); + + auto nonce = FromHex<16>(digestData.nonce); + + input.Storage().clear(); + input << infra::AsHex(hash1) << ":" << infra::AsHex(nonce) << ":" << digestData.nc << ":" << digestData.cnonce << ":auth:" << infra::AsHex(hash2); + auto computedResponse = sha.Calculate(infra::StringAsByteRange(input.Storage())); + infra::StringOutputStream::WithStorage<64> responseStream; + responseStream << infra::AsHex(computedResponse); + digestData.response = responseStream.Storage(); + + auto oldAuthenticationResponse = authenticationResponse; + authenticationResponse.clear(); + infra::StringOutputStream stream(authenticationResponse); + stream << "Digest "; + Stream(stream, "username", digestData.username, true); + Stream(stream, "realm", digestData.realm, true); + Stream(stream, "nonce", digestData.nonce, false); + Stream(stream, "uri", digestData.uri, true); + Stream(stream, "qop", digestData.qop, false); + Stream(stream, "algorithm", "SHA-256", false); + Stream(stream, "nc", digestData.nc, false); + Stream(stream, "cnonce", digestData.cnonce, true); + Stream(stream, "response", digestData.response, true); + authenticationResponse.pop_back(); + + ++retries; + retry = oldAuthenticationResponse != authenticationResponse && retries < 3; + } + } +} diff --git a/postmaster/flex/HttpClientAuthenticationDigest.hpp b/postmaster/flex/HttpClientAuthenticationDigest.hpp new file mode 100644 index 0000000..ffa4b03 --- /dev/null +++ b/postmaster/flex/HttpClientAuthenticationDigest.hpp @@ -0,0 +1,43 @@ +#ifndef POSTMASTER_HTTP_CLIENT_AUTHENTICATION_DIGEST_HPP +#define POSTMASTER_HTTP_CLIENT_AUTHENTICATION_DIGEST_HPP + +#include "hal/synchronous_interfaces/SynchronousRandomDataGenerator.hpp" +#include "services/network/HttpClientAuthentication.hpp" +#include "services/util/Sha256MbedTls.hpp" + +namespace application +{ + class HttpClientAuthenticationDigest + : public services::HttpClientAuthentication + { + public: + template + using WithMaxHeaders = infra::WithStorage::WithMaxSize>; + + HttpClientAuthenticationDigest(infra::BoundedVector& headersWithAuthorization, infra::BoundedConstString password, hal::SynchronousRandomDataGenerator& randomDataGenerator); + + protected: + // Implementation of HttpClientAuthentication + void Authenticate(services::HttpVerb verb, infra::BoundedConstString target, infra::BoundedConstString scheme, infra::BoundedConstString value) override; + infra::BoundedConstString AuthenticationHeader() const override; + bool Retry() const override; + void Reset() override; + + private: + void ComputeAuthenticationResponse(services::HttpVerb verb, infra::BoundedConstString target, infra::BoundedConstString keyValues); + + private: + const services::Sha256MbedTls sha; + infra::BoundedConstString password; + hal::SynchronousRandomDataGenerator& randomDataGenerator; + + uint32_t nc = 1; + infra::BoundedString::WithStorage<8> ncString; + infra::BoundedString::WithStorage<32> cnonceString; + infra::BoundedString::WithStorage<1024> authenticationResponse; + bool retry = false; + int retries = 0; + }; +} + +#endif diff --git a/postmaster/flex/Main.cpp b/postmaster/flex/Main.cpp new file mode 100644 index 0000000..368d682 --- /dev/null +++ b/postmaster/flex/Main.cpp @@ -0,0 +1,64 @@ +#include "args.hxx" +#include "hal/generic/FileSystemGeneric.hpp" +#include "hal/generic/SynchronousRandomDataGeneratorGeneric.hpp" +#include "hal/generic/TimerServiceGeneric.hpp" +#include "infra/timer/TimeStreaming.hpp" +#include "postmaster/flex/EchoWebSocketClientFactory.hpp" +#include "postmaster/flex/FlexHttpClient.hpp" +#include "postmaster/flex/HttpClientAuthenticationDigest.hpp" +#include "services/network/ConnectionFactoryWithNameResolver.hpp" +#include "services/network/HttpClientImpl.hpp" +#include "services/network_instantiations/NetworkAdapter.hpp" +#include "services/tracer/GlobalTracer.hpp" +#include "services/tracer/TracerOnIoOutputInfrastructure.hpp" + +int main(int argc, const char* argv[], const char* env[]) +{ + args::ArgumentParser parser("Firmware Lifecycle and ECHO eXchange"); + args::Group positionals(parser, "Positional arguments:"); + args::Positional urlArgument(positionals, "url", "Postmaster url to connect to", args::Options::Required); + args::Positional firmwareArgument(positionals, "firmware", "Binary firmware with which the target attached to Postmaster is upgraded"); + args::Group flags(parser, "Optional flags:"); + args::ValueFlag passwordArgument(flags, "password", "Password of Postmaster", { 'p', "password" }); + args::HelpFlag help(parser, "help", "display this help menu.", { 'h', "help" }); + + try + { + parser.ParseCLI(argc, argv); + + static hal::TimerServiceGeneric timerService; + static hal::SynchronousRandomDataGeneratorGeneric randomDataGenerator; + static main_::TracerOnIoOutputInfrastructure tracer; + static main_::NetworkAdapter network; + static hal::FileSystemGeneric fileSystem; + + static auto firmware = firmwareArgument ? fileSystem.ReadBinaryFile(args::get(firmwareArgument)) : std::vector{}; + + static services::HttpClientConnectorWithNameResolverImpl<> connector(network.ConnectionFactoryWithNameResolver()); + static infra::BoundedString::WithStorage<512> httpUrl{ args::get(urlArgument) }; + static infra::BoundedString::WithStorage<512> webSocketUrl{ args::get(urlArgument) }; + static application::EchoWebSocketClientFactory webSocketFactory(webSocketUrl, 80, network.ConnectionFactory(), tracer.tracer); + static services::HttpClientWebSocketInitiation webSocketInitiation(webSocketFactory, connector, + webSocketFactory, randomDataGenerator, services::noAutoConnect); + static application::HttpClientAuthenticationDigest::WithMaxHeaders<10> clientAuthentication{ args::get(passwordArgument), randomDataGenerator }; + static services::HttpClientAuthenticationConnector clientAuthenticationConnector{ connector, clientAuthentication }; + static application::FlexHttpClient httpClient(httpUrl, 80, clientAuthenticationConnector, firmware, webSocketInitiation, tracer.tracer); + + network.ExecuteUntil([&]() + { + return !network.NetworkActivity() && timerService.NextTrigger() == infra::TimePoint::max(); + }); + } + catch (const args::Help&) + { + std::cout << parser; + return 1; + } + catch (const std::exception& ex) + { + std::cout << ex.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/postmaster/frontend/AuthenticatedHttpPage.cpp b/postmaster/frontend/AuthenticatedHttpPage.cpp new file mode 100644 index 0000000..0158320 --- /dev/null +++ b/postmaster/frontend/AuthenticatedHttpPage.cpp @@ -0,0 +1,194 @@ +#include "postmaster/frontend/AuthenticatedHttpPage.hpp" +#include "infra/stream/StringInputStream.hpp" +#include "infra/stream/StringOutputStream.hpp" +#include "postmaster/frontend/CommaSeparatedKeyValueListParser.hpp" +#include "services/network/HttpErrors.hpp" + +namespace application +{ + Authentication::Authentication(services::ConfigurationStoreAccess password, hal::SynchronousRandomDataGenerator& randomDataGenerator) + : password(password) + , randomDataGenerator(randomDataGenerator) + { + GenerateNonce(); + } + + void Authentication::GenerateNonce() + { + nc = 0; + randomDataGenerator.GenerateRandomData(nonce); + } + + bool Authentication::Enabled() const + { + return !password->empty(); + } + + bool Authentication::AuthorizedPassword(infra::BoundedConstString password) const + { + return password == *this->password; + } + + bool Authentication::Authorized(const DigestData& digestData, infra::BoundedConstString method) + { + infra::StringInputStream ncStream(digestData.nc, infra::noFail); + uint32_t digestNc; + ncStream >> infra::hex >> digestNc; + if (digestNc <= nc) + return false; + nc = digestNc; + + infra::StringOutputStream::WithStorage<256> input; + input << digestData.username << ":postmaster:" << *password; + auto hash1 = sha.Calculate(infra::StringAsByteRange(input.Storage())); + + input.Storage().clear(); + input << method << ":" << digestData.uri; + auto hash2 = sha.Calculate(infra::StringAsByteRange(input.Storage())); + + auto nonce = FromHex<16>(digestData.nonce); + + input.Storage().clear(); + input << infra::AsHex(hash1) << ":" << infra::AsHex(nonce) << ":" << digestData.nc << ":" << digestData.cnonce << ":auth:" << infra::AsHex(hash2); + auto computedResponse = sha.Calculate(infra::StringAsByteRange(input.Storage())); + + auto response = FromHex<32>(digestData.response); + + return infra::ContentsEqual(infra::MakeRange(response), infra::MakeRange(computedResponse)); + } + + infra::ConstByteRange Authentication::Nonce() const + { + return nonce; + } + + AuthenticatedHttpPage::AuthenticatedHttpPage(Authentication& authentication, services::HttpPage& page) + : authentication(authentication) + , page(page) + {} + + bool AuthenticatedHttpPage::ServesRequest(const infra::Tokenizer& pathTokens) const + { + return page.ServesRequest(pathTokens); + } + + void AuthenticatedHttpPage::RequestReceived(services::HttpRequestParser& parser, services::HttpServerConnection& connection) + { + infra::BoundedConstString authorization = parser.Header("Authorization"); + + authorized = false; + + if (!authentication.Enabled()) + ReceivedAuthorizedRequest(parser, connection); + else if (authentication.AuthorizedPassword(PasswordFromQuery(parser.Query()))) + ReceivedAuthorizedRequest(parser, connection); + else if (authorization.empty()) + SendUnauthorizedResponse(connection); + else + RequestReceivedWithAuthorization(parser, connection, authorization); + } + + void AuthenticatedHttpPage::DataReceived(infra::SharedPtr&& reader) + { + if (authorized) + page.DataReceived(std::move(reader)); + } + + void AuthenticatedHttpPage::Close() + { + if (authorized) + page.Close(); + } + + infra::BoundedConstString AuthenticatedHttpPage::Status() const + { + return "401 Unauthorized"; + } + + void AuthenticatedHttpPage::WriteBody(infra::TextOutputStream& stream) const + {} + + infra::BoundedConstString AuthenticatedHttpPage::ContentType() const + { + return "text/plain"; + } + + void AuthenticatedHttpPage::AddHeaders(services::HttpResponseHeaderBuilder& builder) const + { + infra::StringOutputStream::WithStorage<256> challenge; + challenge << R"(Digest realm="postmaster", qop="auth", algorithm=SHA-256, nonce=")" << infra::AsHex(authentication.Nonce()) << R"(")"; + builder.AddHeader("WWW-Authenticate", challenge.Storage()); + } + + infra::BoundedConstString AuthenticatedHttpPage::PasswordFromQuery(infra::BoundedConstString queryString) + { + infra::Tokenizer queries(queryString, '&'); + + for (auto i = 0; i != queries.Size(); ++i) + { + infra::Tokenizer queryTokens(queries.Token(i), '='); + if (queryTokens.Size() == 2 && queryTokens.Token(0) == "p") + return queryTokens.Token(1); + } + + return {}; + } + + void AuthenticatedHttpPage::RequestReceivedWithAuthorization(services::HttpRequestParser& parser, services::HttpServerConnection& connection, infra::BoundedConstString authorization) + { + static const infra::BoundedConstString scheme = "Digest "; + + if (authorization.find(scheme) == 0) + { + auto keyValues = authorization.substr(scheme.size()); + Authentication::DigestData digestData; + CommaSeparatedKeyValueListParser keyValueParser(keyValues, [&digestData](infra::BoundedConstString key, infra::BoundedConstString value) + { + if (key == "username") + digestData.username = value; + if (key == "realm") + digestData.realm = value; + if (key == "nonce") + digestData.nonce = value; + if (key == "uri") + digestData.uri = value; + if (key == "qop") + digestData.qop = value; + if (key == "nc") + digestData.nc = value; + if (key == "cnonce") + digestData.cnonce = value; + if (key == "response") + digestData.response = value; + }); + + if (keyValueParser.Valid()) + { + RequestReceivedWithData(parser, connection, digestData); + return; + } + } + + connection.SendResponse(services::HttpResponseBadRequest::Instance()); + } + + void AuthenticatedHttpPage::RequestReceivedWithData(services::HttpRequestParser& parser, services::HttpServerConnection& connection, const Authentication::DigestData& digestData) + { + if (authentication.Authorized(digestData, services::HttpVerbToString(parser.Verb()))) + ReceivedAuthorizedRequest(parser, connection); + else + SendUnauthorizedResponse(connection); + } + + void AuthenticatedHttpPage::ReceivedAuthorizedRequest(services::HttpRequestParser& parser, services::HttpServerConnection& connection) + { + authorized = true; + page.RequestReceived(parser, connection); + } + + void AuthenticatedHttpPage::SendUnauthorizedResponse(services::HttpServerConnection& connection) + { + authentication.GenerateNonce(); + connection.SendResponse(*this); + } +} diff --git a/postmaster/frontend/AuthenticatedHttpPage.hpp b/postmaster/frontend/AuthenticatedHttpPage.hpp new file mode 100644 index 0000000..624d717 --- /dev/null +++ b/postmaster/frontend/AuthenticatedHttpPage.hpp @@ -0,0 +1,104 @@ +#ifndef POSTMASTER_AUTHENTICATED_HTTP_PAGE_HPP +#define POSTMASTER_AUTHENTICATED_HTTP_PAGE_HPP + +#include "hal/synchronous_interfaces/SynchronousRandomDataGenerator.hpp" +#include "infra/stream/StringInputStream.hpp" +#include "services/network/HttpServer.hpp" +#include "services/util/ConfigurationStore.hpp" +#include "services/util/Sha256MbedTls.hpp" + +namespace application +{ + class Authentication + { + public: + struct DigestData + { + infra::BoundedConstString username; + infra::BoundedConstString realm; + infra::BoundedConstString nonce; + infra::BoundedConstString uri; + infra::BoundedConstString qop; + infra::BoundedConstString nc; + infra::BoundedConstString cnonce; + infra::BoundedConstString response; + }; + + public: + Authentication(services::ConfigurationStoreAccess password, hal::SynchronousRandomDataGenerator& randomDataGenerator); + + void GenerateNonce(); + bool Enabled() const; + bool AuthorizedPassword(infra::BoundedConstString password) const; + bool Authorized(const DigestData& digestData, infra::BoundedConstString method); + infra::ConstByteRange Nonce() const; + + private: + services::ConfigurationStoreAccess password; + hal::SynchronousRandomDataGenerator& randomDataGenerator; + + uint32_t nc = 0; + std::array nonce; + + const services::Sha256MbedTls sha; + }; + + class AuthenticatedHttpPage + : public services::HttpPage + , public services::HttpResponse + { + public: + template + struct WithPage; + + AuthenticatedHttpPage(Authentication& authentication, services::HttpPage& page); + + // Implementation of HttpPage + bool ServesRequest(const infra::Tokenizer& pathTokens) const override; + void RequestReceived(services::HttpRequestParser& parser, services::HttpServerConnection& connection) override; + void DataReceived(infra::SharedPtr&& reader) override; + void Close() override; + + // Implementation of HttpResponse + infra::BoundedConstString Status() const override; + void WriteBody(infra::TextOutputStream& stream) const override; + infra::BoundedConstString ContentType() const override; + void AddHeaders(services::HttpResponseHeaderBuilder& builder) const override; + + private: + infra::BoundedConstString PasswordFromQuery(infra::BoundedConstString queryString); + void RequestReceivedWithAuthorization(services::HttpRequestParser& parser, services::HttpServerConnection& connection, infra::BoundedConstString authorization); + void RequestReceivedWithData(services::HttpRequestParser& parser, services::HttpServerConnection& connection, const Authentication::DigestData& digestData); + void ReceivedAuthorizedRequest(services::HttpRequestParser& parser, services::HttpServerConnection& connection); + void SendUnauthorizedResponse(services::HttpServerConnection& connection); + + private: + Authentication& authentication; + services::HttpPage& page; + bool authorized = false; + }; + + template + struct AuthenticatedHttpPage::WithPage + : AuthenticatedHttpPage + { + template + WithPage(Authentication& authentication, Args&&... args) + : AuthenticatedHttpPage(authentication, page) + , page(std::forward(args)...) + {} + + Page page; + }; + + template + std::array FromHex(infra::BoundedConstString from) + { + std::array result; + infra::StringInputStream reader(from, infra::noFail); + reader >> infra::FromHex(result); + return result; + } +} + +#endif diff --git a/postmaster/frontend/CMakeLists.txt b/postmaster/frontend/CMakeLists.txt new file mode 100644 index 0000000..46f3c37 --- /dev/null +++ b/postmaster/frontend/CMakeLists.txt @@ -0,0 +1,143 @@ +function(minify_html) + # html-minifier can be installed with: + # npm install -g html-minifier + if (CMAKE_HOST_WIN32) + find_program(HTML_MINIFIER NAMES html-minifier.cmd PATHS $ENV{APPDATA} PATH_SUFFIXES npm) + else() + find_program(HTML_MINIFIER NAMES html-minifier) + endif() + + if (HTML_MINIFIER) + foreach (file ${ARGV}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${file} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${file} + COMMAND ${HTML_MINIFIER} + ARGS --collapse-whitespace + --remove-comments + --remove-optional-tags + --remove-redundant-attributes + --remove-script-type-attributes + --remove-tag-whitespace + --use-short-doctype + --minify-css true + --minify-js true + ${CMAKE_CURRENT_SOURCE_DIR}/${file} -o ${CMAKE_CURRENT_BINARY_DIR}/${file} + ) + endforeach() + else() + foreach (file ${ARGV}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${file} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${file} + COMMAND ${CMAKE_COMMAND} + ARGS -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/${file} ${CMAKE_CURRENT_BINARY_DIR}/${file} + ) + endforeach() + endif() +endfunction() + +function(minify_css) + # csso can be installed with: + # npm install -g csso-cli + if (CMAKE_HOST_WIN32) + find_program(CSS_MINIFIER NAMES csso.cmd PATHS $ENV{APPDATA} PATH_SUFFIXES npm) + else() + find_program(CSS_MINIFIER NAMES csso) + endif() + + if (CSS_MINIFIER) + foreach (file ${ARGV}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${file} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${file} + COMMAND ${CSS_MINIFIER} + ARGS ${CMAKE_CURRENT_SOURCE_DIR}/${file} -o ${CMAKE_CURRENT_BINARY_DIR}/${file} + ) + endforeach() + else() + foreach (file ${ARGV}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${file} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${file} + COMMAND ${CMAKE_COMMAND} + ARGS -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/${file} ${CMAKE_CURRENT_BINARY_DIR}/${file} + ) + endforeach() + endif() +endfunction() + +function(minify_js) + # babel-minify can be installed with: + # npm install -g babel-minify + if (CMAKE_HOST_WIN32) + find_program(JS_MINIFIER NAMES minify.cmd PATHS $ENV{APPDATA} PATH_SUFFIXES npm) + else() + find_program(JS_MINIFIER NAMES minify) + endif() + + if (JS_MINIFIER) + foreach (file ${ARGV}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${file} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${file} + COMMAND ${JS_MINIFIER} + ARGS ${CMAKE_CURRENT_SOURCE_DIR}/${file} -o ${CMAKE_CURRENT_BINARY_DIR}/${file} + ) + endforeach() + else() + foreach (file ${ARGV}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${file} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${file} + COMMAND ${CMAKE_COMMAND} + ARGS -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/${file} ${CMAKE_CURRENT_BINARY_DIR}/${file} + ) + endforeach() + endif() +endfunction() + +function(minify_web_resources) + foreach(file ${ARGV}) + cmake_path(GET file EXTENSION LAST_ONLY extension) + + if ("${extension}" STREQUAL .html) + minify_html(${file}) + elseif ("${extension}" STREQUAL .css) + minify_css(${file}) + elseif ("${extension}" STREQUAL .js) + minify_js(${file}) + endif() + endforeach() +endfunction() + +add_library(postmaster.frontend STATIC) + +target_include_directories(postmaster.frontend PUBLIC + "$" + "$" + "$" +) + +target_sources(postmaster.frontend PRIVATE + AuthenticatedHttpPage.cpp + AuthenticatedHttpPage.hpp + CommaSeparatedKeyValueListParser.cpp + CommaSeparatedKeyValueListParser.hpp + ReportingHttpServer.cpp + ReportingHttpServer.hpp +) + +minify_web_resources( + index.html + style.css + upload.js +) + +emil_target_string_source(postmaster.frontend ${CMAKE_CURRENT_BINARY_DIR}/index.html indexHtml page_contents IndexHtml) +emil_target_string_source(postmaster.frontend ${CMAKE_CURRENT_BINARY_DIR}/style.css styleCss page_contents StyleCss) +emil_target_string_source(postmaster.frontend ${CMAKE_CURRENT_BINARY_DIR}/upload.js uploadJs page_contents UploadJs) + +target_link_libraries(postmaster.frontend PUBLIC + infra.util +) diff --git a/postmaster/frontend/CommaSeparatedKeyValueListParser.cpp b/postmaster/frontend/CommaSeparatedKeyValueListParser.cpp new file mode 100644 index 0000000..e72cd58 --- /dev/null +++ b/postmaster/frontend/CommaSeparatedKeyValueListParser.cpp @@ -0,0 +1,105 @@ +#include "postmaster/frontend/CommaSeparatedKeyValueListParser.hpp" + +namespace application +{ + CommaSeparatedKeyValueListParser::CommaSeparatedKeyValueListParser(infra::BoundedConstString list, const infra::Function& foundKeyValue) + : list(list) + , foundKeyValue(foundKeyValue) + { + Run(); + } + + bool CommaSeparatedKeyValueListParser::Valid() const + { + return valid; + } + + void CommaSeparatedKeyValueListParser::Run() + { + while (valid) + { + ConsumeSpace(); + key = ReadToken(); + ConsumeSpace(); + ConsumeAssign(); + ConsumeSpace(); + value = ReadValue(); + ConsumeSpace(); + + foundKeyValue(key, value); + + if (list.empty()) + break; + + ConsumeComma(); + ConsumeSpace(); + } + } + + void CommaSeparatedKeyValueListParser::ConsumeSpace() + { + while (!list.empty() && infra::BoundedConstString(" \r\n\t").find(list.front()) != infra::BoundedConstString::npos) + list = list.substr(1); + } + + void CommaSeparatedKeyValueListParser::ConsumeAssign() + { + if (list.empty() || list.front() != '=') + valid = false; + else + list = list.substr(1); + } + + void CommaSeparatedKeyValueListParser::ConsumeComma() + { + if (list.empty() || list.front() != ',') + valid = false; + else + list = list.substr(1); + } + + void CommaSeparatedKeyValueListParser::ConsumeQuote() + { + if (list.empty() || list.front() != '"') + valid = false; + else + list = list.substr(1); + } + + infra::BoundedConstString CommaSeparatedKeyValueListParser::ReadToken() + { + auto result = list.substr(0, std::distance(list.begin(), std::find_if(list.begin(), list.end(), [](char c) + { + return c < 32 || c >= 127 || infra::BoundedConstString("()<>@,;:\\\"/[]?={} \t").find(c) != infra::BoundedConstString::npos; + }))); + list = list.substr(result.size()); + valid = valid && !result.empty(); + return result; + } + + infra::BoundedConstString CommaSeparatedKeyValueListParser::ReadValue() + { + if (!list.empty() && list.front() == '"') + { + bool quoted = false; + ConsumeQuote(); + auto result = list.substr(0, std::distance(list.begin(), std::find_if(list.begin(), list.end(), ["ed](char c) + { + if (quoted) + { + quoted = false; + return false; + } + + quoted = c == '\\'; + return c == '"'; + }))); + valid = valid && !result.empty(); + list = list.substr(result.size()); + ConsumeQuote(); + return result; + } + else + return ReadToken(); + } +} diff --git a/postmaster/frontend/CommaSeparatedKeyValueListParser.hpp b/postmaster/frontend/CommaSeparatedKeyValueListParser.hpp new file mode 100644 index 0000000..250d8cc --- /dev/null +++ b/postmaster/frontend/CommaSeparatedKeyValueListParser.hpp @@ -0,0 +1,35 @@ +#ifndef POSTMASTER_COMMA_SEPARATED_KEY_VALUE_LIST_PARSER_HPP +#define POSTMASTER_COMMA_SEPARATED_KEY_VALUE_LIST_PARSER_HPP + +#include "infra/util/BoundedString.hpp" +#include "infra/util/Function.hpp" + +namespace application +{ + class CommaSeparatedKeyValueListParser + { + public: + CommaSeparatedKeyValueListParser(infra::BoundedConstString list, const infra::Function& foundKeyValue); + + bool Valid() const; + + private: + void Run(); + void ConsumeSpace(); + void ConsumeAssign(); + void ConsumeComma(); + void ConsumeQuote(); + infra::BoundedConstString ReadToken(); + infra::BoundedConstString ReadValue(); + + private: + infra::BoundedConstString list; + infra::Function foundKeyValue; + bool valid = true; + + infra::BoundedConstString key; + infra::BoundedConstString value; + }; +} + +#endif diff --git a/postmaster/frontend/ReportingHttpServer.cpp b/postmaster/frontend/ReportingHttpServer.cpp new file mode 100644 index 0000000..19f64f3 --- /dev/null +++ b/postmaster/frontend/ReportingHttpServer.cpp @@ -0,0 +1,26 @@ +#include "postmaster/frontend/ReportingHttpServer.hpp" + +namespace application +{ + ReportingHttpServerConnectionObserver::ReportingHttpServerConnectionObserver(infra::BoundedString& buffer, HttpPageServer& httpServer, const infra::Function& reporter, services::IPAddress address) + : services::HttpServerConnectionObserver(buffer, httpServer) + , reporter(reporter) + { + reporter(true, address); + } + + ReportingHttpServerConnectionObserver::~ReportingHttpServerConnectionObserver() + { + reporter(false, services::IPAddress()); + } + + ReportingHttpServer::ReportingHttpServer(infra::BoundedString& buffer, services::ConnectionFactory& connectionFactory, uint16_t port, const infra::Function& reporter) + : SingleConnectionListener(connectionFactory, port, { connectionCreator }) + , reporter(reporter) + , buffer(buffer) + , connectionCreator([this](infra::Optional& value, services::IPAddress address) + { + value.Emplace(this->buffer, *this, this->reporter, address); + }) + {} +} diff --git a/postmaster/frontend/ReportingHttpServer.hpp b/postmaster/frontend/ReportingHttpServer.hpp new file mode 100644 index 0000000..5672690 --- /dev/null +++ b/postmaster/frontend/ReportingHttpServer.hpp @@ -0,0 +1,36 @@ +#ifndef POSTMASTER_REPORTING_HTTP_SERVER_HPP +#define POSTMASTER_REPORTING_HTTP_SERVER_HPP + +#include "services/network/HttpServer.hpp" + +namespace application +{ + class ReportingHttpServerConnectionObserver + : public services::HttpServerConnectionObserver + { + public: + ReportingHttpServerConnectionObserver(infra::BoundedString& buffer, HttpPageServer& httpServer, const infra::Function& reporter, services::IPAddress address); + ~ReportingHttpServerConnectionObserver(); + + private: + infra::Function reporter; + }; + + class ReportingHttpServer + : public services::SingleConnectionListener + , public services::HttpPageServer + { + public: + template<::size_t BufferSize> + using WithBuffer = infra::WithStorage>; + + ReportingHttpServer(infra::BoundedString& buffer, services::ConnectionFactory& connectionFactory, uint16_t port, const infra::Function& reporter); + + private: + infra::Function reporter; + infra::BoundedString& buffer; + infra::Creator connectionCreator; + }; +} + +#endif diff --git a/postmaster/frontend/index.html b/postmaster/frontend/index.html new file mode 100644 index 0000000..d9b607e --- /dev/null +++ b/postmaster/frontend/index.html @@ -0,0 +1,81 @@ + + + + + + Postmaster (STM32 Firmware Uploader) + + + + + + +
+
+
+
+ Firmware Upload + +
+ + +
or drop files here
+
+ + + + + +
+
+ +
+ +
+

Status Messages

+
+
+ +
+

Postmasters

+ + + + + + + + + + + + +
Discovered Postmasters
HostnameVersionAttributes
+
+ +
+

Settings

+ + + + + + +
+ +
+

Postmaster version ${version}

+
+
+ + + + + diff --git a/postmaster/frontend/style.css b/postmaster/frontend/style.css new file mode 100644 index 0000000..557f8fc --- /dev/null +++ b/postmaster/frontend/style.css @@ -0,0 +1,125 @@ +body { + background-color: #fff; + color: #333; + font-family: "Segoe UI", Tahoma, Helvetica, freesans, sans-serif; + font-size: 90%; + margin: 0; +} + +ul.topnav { + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; + background-color: #333; +} + +ul.topnav li { + float: left; +} + +ul.topnav li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +ul.topnav li a:hover:not(.active) { + background-color: #111; +} + +ul.topnav li a.active { + background-color: #04AA6D; +} + +ul.topnav li.right { + float: right; +} + +@media screen and (max-width: 600px) { + ul.topnav li.right, + ul.topnav li { float: none; } +} + +h1, h2 { + font-size: 1.5em; + font-weight: normal; +} + +h2 { + font-size: 1.3em; +} + +legend { + font-weight: bold; + color: #333; +} + +#filedrag { + display: none; + font-weight: bold; + text-align: center; + padding: 1em 0; + margin: 1em 0; + color: #555; + border: 2px dashed #555; + border-radius: 7px; + cursor: default; +} + +#filedrag.hover { + color: #f00; + border-color: #f00; + border-style: solid; + box-shadow: inset 0 3px 4px #888; +} + +img { + max-width: 100%; +} + +pre { + width: 95%; + height: 8em; + font-family: monospace; + font-size: 0.9em; + padding: 1px 2px; + margin: 0 0 1em auto; + border: 1px inset #666; + background-color: #eee; + overflow: auto; +} + +#content { + margin: 10px; +} + +.hide { + display: none; +} + +#messages { + padding: 0 10px; + margin: 1em 0; + border: 1px solid #999; +} + +#progress p { + display: block; + width: 240px; + padding: 2px 5px; + margin: 2px 0; + border: 1px inset #446; + border-radius: 5px; + background: #eee; +} + +#progress p.success { + background: #0c0 none 0 0 no-repeat; +} + +#progress p.failed { + background: #c00 none 0 0 no-repeat; +} diff --git a/postmaster/frontend/upload.js b/postmaster/frontend/upload.js new file mode 100644 index 0000000..8a6401f --- /dev/null +++ b/postmaster/frontend/upload.js @@ -0,0 +1,184 @@ +/* jslint browser:true */ + +(function ($) { + $.each(['show', 'hide'], function (_, ev) { + var el = $.fn[ev]; + $.fn[ev] = function () { + this.trigger(ev); + return el.apply(this, arguments); + }; + }); +})(jQuery); + +(async function () { + function $id(id) { + return document.getElementById(id); + } + + function Output(msg) { + var m = $id("messages"); + m.innerHTML = msg + m.innerHTML; + } + + function FileDragHover(e) { + e.stopPropagation(); + e.preventDefault(); + e.target.className = (e.type === "dragover" ? "hover" : ""); + } + + function FileSelectHandler(e) { + FileDragHover(e); + + var files = e.target.files || e.dataTransfer.files; + + for (var i = 0, f; f = files[i]; i++) { + ParseFile(f); + UploadFile(f); + } + } + + function ParseFile(file) { + Output(`

File information: ${file.name} type: ${file.type}size: ${file.size} bytes

`); + } + + function UploadFile(file) { + var xhr = new XMLHttpRequest(); + if (xhr.upload) { + var o = $id("progress"); + var progress = o.appendChild(document.createElement("p")); + progress.appendChild(document.createTextNode("upload " + file.name)); + + xhr.upload.addEventListener("progress", function (e) { + var pc = parseInt(100 - (e.loaded / e.total * 100)); + progress.style.backgroundPosition = pc + "% 0"; + }, false); + + xhr.onreadystatechange = function (e) { + if (xhr.readyState == 4) { + progress.className = (xhr.status == 200 ? "success" : "failure"); + + if (xhr.status != 200) { + Output("

Upload " + xhr.statusText + ": " + xhr.responseText + "

") + } + } + }; + + var target = $("#radio_target").prop("checked") ? "firmware/target" : "firmware/self"; + + xhr.open("POST", target, true); + xhr.send(file); + } + } + + function Init() { + var fileselect = $id("fileselect"), + filedrag = $id("filedrag"); + + fileselect.addEventListener("change", FileSelectHandler, false); + + var xhr = new XMLHttpRequest(); + if (xhr.upload) { + filedrag.addEventListener("dragover", FileDragHover, false); + filedrag.addEventListener("dragleave", FileDragHover, false); + filedrag.addEventListener("drop", FileSelectHandler, false); + filedrag.style.display = "block"; + } + } + + if (window.File && window.FileList && window.FileReader) { + Init(); + + let response = await fetch('configuration'); + let configuration = await response.json(); + var timer; + + $(".topnav li").each(function(i) { + if (i > 0) { $("#content").children("div:eq('" + i + "')").hide(); } + }); + + $(".topnav li > a").each(function(i) { + $(this).click(function() { + $(".topnav li > a").removeClass('active'); + $(this).addClass('active'); + $("#content").children("div:eq('" + i + "')").show().siblings().hide(); + }); + }); + + $('#about').on('show', function() { + $('#about h1').html($('#about h1').html().replace('${version}', configuration.version_full)) + }); + + $('#discovery').on('show', async function() { + timer = setInterval(async function() { + let response = await fetch('discovery'); + let discovery = await response.json(); + + $('#postmasters tbody').empty(); + discovery.devices.forEach((device) => { + $('#postmasters tbody').append(`${device.hostname}${device.version}${device.attributes}`); + }); + }, 5000); + }); + + $('#discovery').on('hide', function() { + clearInterval(timer); + }); + + $('#hostname').val(configuration.hostname); + $('#hostname').blur(async function() { + const data = { + hostname: $('#hostname').val() + } + + const options = { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + } + + let response = await fetch('configuration', options); + let configuration = await response.json(); + $('#hostname').val(configuration.hostname); + }); + + $('#attributes').val(configuration.attributes); + $('#attributes').blur(async function() { + const data = { + attributes: $('#attributes').val() + } + + const options = { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + } + + let response = await fetch('configuration', options); + let configuration = await response.json(); + $('#attributes').val(configuration.attributes); + }); + + $('#password').val(configuration.password); + $('#password').blur(async function () { + const data = { + password: $('#password').val() + } + + const options = { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + } + + let response = await fetch('configuration', options); + let configuration = await response.json(); + $('#password').val(configuration.password); + }); + } +})(); diff --git a/postmaster/instantiations/CMakeLists.txt b/postmaster/instantiations/CMakeLists.txt new file mode 100644 index 0000000..1eace4e --- /dev/null +++ b/postmaster/instantiations/CMakeLists.txt @@ -0,0 +1,51 @@ +add_library(postmaster.instantiations STATIC) + +target_include_directories(postmaster.instantiations PUBLIC + "$" + "$" +) + +protocol_buffer_echo_cpp(postmaster.instantiations + PostmasterConfiguration.proto +) + +target_sources(postmaster.instantiations PRIVATE + Configuration.cpp + Configuration.hpp + EchoServer.cpp + EchoServer.hpp + HttpPageConfiguration.cpp + HttpPageConfiguration.hpp + HttpPageDiscovery.cpp + HttpPageDiscovery.hpp + HttpServer.cpp + HttpServer.hpp + HttpServerFrontEnd.cpp + HttpServerFrontEnd.hpp + Mdns.cpp + Mdns.hpp + MdnsDiscovery.cpp + MdnsDiscovery.hpp + SelfProgrammer.hpp + TargetProgrammer.hpp + TargetUart.cpp + TargetUart.hpp + TargetUartEcho.cpp + TargetUartEcho.hpp + UartCreator.hpp + ViewNetworkStatus.cpp + ViewNetworkStatus.hpp + ViewStatus.cpp + ViewStatus.hpp +) + +add_version_header_dependency(postmaster.instantiations postmaster.version) + +target_link_libraries(postmaster.instantiations PUBLIC + infra.syntax + postmaster.programmer + postmaster.frontend + preview.views + preview.fonts + lwip.lwip_cpp +) diff --git a/postmaster/instantiations/Configuration.cpp b/postmaster/instantiations/Configuration.cpp new file mode 100644 index 0000000..8ffd843 --- /dev/null +++ b/postmaster/instantiations/Configuration.cpp @@ -0,0 +1,52 @@ +#include "postmaster/instantiations/Configuration.hpp" +#include "infra/stream/StringOutputStream.hpp" +#include "mbedtls/sha256.h" + +namespace main_ +{ + Configuration::Configuration(hal::Flash& flash0, hal::Flash& flash1, infra::ConstByteRange id, const infra::Function& onRecovered, services::Tracer& tracer) + : id(id) + , onRecovered(onRecovered) + , store(flash0, flash1, sha256, [this, &tracer](bool success) + { + tracer.Trace() << "Configuration load " << (success ? "successful" : "failed"); + if (!success) + { + tracer.Trace() << "Loading default configuration"; + LoadDefaultConfiguration(store.Configuration()); + store.Write(); + } + + tracer.Trace() << "Hostname: " << store.Configuration().hostname; + + this->onRecovered(); + }) + {} + + void Configuration::LoadDefaultConfiguration(postmaster_configuration::Configuration& configuration) + { + configuration.hostname = DeviceHostname(); + } + + hal::MacAddress Configuration::DeviceMacAddress() const + { + std::array hashedDeviceId; + int r = mbedtls_sha256(reinterpret_cast(id.begin()), id.size(), reinterpret_cast(hashedDeviceId.data()), 0); + assert(r == 0); + + hal::MacAddress result; + infra::Copy(infra::Head(infra::MakeRange(hashedDeviceId), result.size()), infra::MakeRange(result)); + result.front() &= 0xfe; // unicast + result.front() |= 0x02; // locally administered + + return result; + } + + infra::BoundedString::WithStorage<32> Configuration::DeviceHostname() const + { + auto macAddress = DeviceMacAddress(); + infra::StringOutputStream::WithStorage<32> hostname; + hostname << "postmaster-" << infra::hex << macAddress[0] << macAddress[1] << macAddress[2] << macAddress[3] << macAddress[4] << macAddress[5]; + return hostname.Storage(); + } +} diff --git a/postmaster/instantiations/Configuration.hpp b/postmaster/instantiations/Configuration.hpp new file mode 100644 index 0000000..19de70d --- /dev/null +++ b/postmaster/instantiations/Configuration.hpp @@ -0,0 +1,27 @@ +#ifndef POSTMASTER_CONFIGURATION_HPP +#define POSTMASTER_CONFIGURATION_HPP + +#include "generated/echo/PostmasterConfiguration.pb.hpp" +#include "hal/interfaces/MacAddress.hpp" +#include "services/tracer/Tracer.hpp" +#include "services/util/ConfigurationStore.hpp" +#include "services/util/Sha256MbedTls.hpp" + +namespace main_ +{ + struct Configuration + { + Configuration(hal::Flash& flash0, hal::Flash& flash1, infra::ConstByteRange id, const infra::Function& onRecovered, services::Tracer& tracer); + + void LoadDefaultConfiguration(postmaster_configuration::Configuration& configuration); + hal::MacAddress DeviceMacAddress() const; + infra::BoundedString::WithStorage<32> DeviceHostname() const; + + infra::ConstByteRange id; + infra::AutoResetFunction onRecovered; + services::Sha256MbedTls sha256; + services::ConfigurationStoreImpl::WithBlobs<> store; + }; +} + +#endif diff --git a/postmaster/instantiations/EchoServer.cpp b/postmaster/instantiations/EchoServer.cpp new file mode 100644 index 0000000..02e0767 --- /dev/null +++ b/postmaster/instantiations/EchoServer.cpp @@ -0,0 +1,66 @@ +#include "postmaster/instantiations/EchoServer.hpp" + +namespace main_ +{ + EchoServer::EchoServer(services::ConnectionFactory& connectionFactory, UartCreator& serialCreator, uint16_t port) + : serialCreator(serialCreator) + , listener(connectionFactory, port, { bridgeCreator }) + {} + + EchoServer::Bridge::Bridge(UartCreator& serialCreator) + : serial(serialCreator, UartConfig{}) + {} + + EchoServer::Bridge::operator services::ConnectionObserver&() + { + return echoConnection; + } + + EchoServer::Bridge::operator const services::ConnectionObserver&() const + { + return echoConnection; + } + + SingleConnectionLink::SingleConnectionLink(services::SingleConnectionListener& listener1, services::SingleConnectionListener& listener2) + : listener1(listener1) + , listener2(listener2) + { + listener1.SetNewConnectionStrategy(*this); + listener2.SetNewConnectionStrategy(*this); + } + + void SingleConnectionLink::StopCurrentConnection(services::SingleConnectionListener& listener) + { + if (&listener == &listener1) + listener1RequestedNewConnection = true; + if (&listener == &listener2) + listener2RequestedNewConnection = true; + + numStopped = 0; + listener1.StopCurrentConnection(listener1); + listener2.StopCurrentConnection(listener2); + } + + void SingleConnectionLink::StartNewConnection() + { + ++numStopped; + + if (numStopped == 2) + { + if (listener1RequestedNewConnection) + { + listener1RequestedNewConnection = false; + listener1.StartNewConnection(); + + if (listener2RequestedNewConnection) + StopCurrentConnection(listener1); + } + else if (listener2RequestedNewConnection) + { + listener2RequestedNewConnection = false; + listener2.StartNewConnection(); + } + } + } + +} diff --git a/postmaster/instantiations/EchoServer.hpp b/postmaster/instantiations/EchoServer.hpp new file mode 100644 index 0000000..0865ccf --- /dev/null +++ b/postmaster/instantiations/EchoServer.hpp @@ -0,0 +1,59 @@ +#include "postmaster/instantiations/UartCreator.hpp" +#include "protobuf/echo/ServiceForwarder.hpp" +#include "services/network/Connection.hpp" +#include "services/network/EchoOnConnection.hpp" +#include "services/network/SingleConnectionListener.hpp" +#include "services/util/EchoInstantiation.hpp" + +namespace main_ +{ + struct EchoServer + { + EchoServer(services::ConnectionFactory& connectionFactory, UartCreator& serialCreator, uint16_t port); + + struct Bridge + { + Bridge(UartCreator& serialCreator); + + infra::ProxyCreator serial; + services::MethodSerializerFactory::ForServices<>::AndProxies<> serializerFactory; + hal::BufferedSerialCommunicationOnUnbuffered::WithStorage<256> bufferedSerial{ *serial }; + main_::EchoOnSesame<256> echoUart{ bufferedSerial, serializerFactory }; + services::EchoOnConnection echoConnection{ serializerFactory }; + + services::ServiceForwarderAll forwardLeft{ echoUart, echoConnection }; + services::ServiceForwarderAll forwardRight{ echoConnection, echoUart }; + + operator services::ConnectionObserver&(); + operator const services::ConnectionObserver&() const; + }; + + UartCreator& serialCreator; + + infra::Creator bridgeCreator{ [this](infra::Optional& value, services::IPAddress address) + { + value.Emplace(serialCreator); + } }; + + services::SingleConnectionListener listener; + }; + + class SingleConnectionLink + : private services::NewConnectionStrategy + { + public: + SingleConnectionLink(services::SingleConnectionListener& listener1, services::SingleConnectionListener& listener2); + + private: + void StopCurrentConnection(services::SingleConnectionListener& listener) override; + void StartNewConnection() override; + + private: + uint32_t numStopped = 0; + services::SingleConnectionListener& listener1; + services::SingleConnectionListener& listener2; + + bool listener1RequestedNewConnection = false; + bool listener2RequestedNewConnection = false; + }; +} diff --git a/postmaster/instantiations/HttpPageConfiguration.cpp b/postmaster/instantiations/HttpPageConfiguration.cpp new file mode 100644 index 0000000..934a8d5 --- /dev/null +++ b/postmaster/instantiations/HttpPageConfiguration.cpp @@ -0,0 +1,66 @@ +#include "postmaster/instantiations/HttpPageConfiguration.hpp" +#include "generated/Version.h" +#include "infra/syntax/JsonFormatter.hpp" +#include "services/network/HttpErrors.hpp" + +namespace application +{ + HttpPageConfiguration::HttpPageConfiguration(infra::BoundedConstString path, services::ConfigurationStoreAccess& hostname, + services::ConfigurationStoreAccess& attributes, services::ConfigurationStoreAccess& password) + : path(path) + , hostname(hostname) + , attributes(attributes) + , password(password) + {} + + bool HttpPageConfiguration::ServesRequest(const infra::Tokenizer& pathTokens) const + { + return pathTokens.TokenAndRest(0) == path; + } + + void HttpPageConfiguration::RespondToRequest(services::HttpRequestParser& parser, services::HttpServerConnection& connection) + { + if (parser.Verb() == services::HttpVerb::get) + connection.SendResponse(*this); + else if (parser.Verb() == services::HttpVerb::post) + { + infra::JsonObject object(parser.BodyBuffer()); + if (object.HasKey("hostname")) + { + object.GetString("hostname").ToString(*hostname); + hostname.Write(); + } + + if (object.HasKey("attributes")) + { + object.GetString("attributes").ToString(*attributes); + attributes.Write(); + } + + if (object.HasKey("password")) + { + object.GetString("password").ToString(*password); + password.Write(); + } + + connection.SendResponse(*this); + } + else + connection.SendResponse(services::HttpResponseBadRequest::Instance()); + } + + infra::BoundedConstString HttpPageConfiguration::Status() const + { + return services::http_responses::ok; + } + + void HttpPageConfiguration::WriteBody(infra::TextOutputStream& stream) const + { + infra::JsonObjectFormatter configuration(stream); + + configuration.Add("hostname", *hostname); + configuration.Add("attributes", *attributes); + configuration.Add("version", Postmaster::generated::VERSION); + configuration.Add("version_full", Postmaster::generated::VERSION_FULL); + } +} diff --git a/postmaster/instantiations/HttpPageConfiguration.hpp b/postmaster/instantiations/HttpPageConfiguration.hpp new file mode 100644 index 0000000..73d82d1 --- /dev/null +++ b/postmaster/instantiations/HttpPageConfiguration.hpp @@ -0,0 +1,33 @@ +#ifndef POSTMASTER_HTTP_PAGE_CONFIGURATION_HPP +#define POSTMASTER_HTTP_PAGE_CONFIGURATION_HPP + +#include "services/network/HttpServer.hpp" +#include "services/util/ConfigurationStore.hpp" + +namespace application +{ + class HttpPageConfiguration + : public services::SimpleHttpPage + , public services::HttpResponse + { + public: + HttpPageConfiguration(infra::BoundedConstString path, services::ConfigurationStoreAccess& hostname, + services::ConfigurationStoreAccess& attributes, services::ConfigurationStoreAccess& password); + + // Implementation of SimpleHttpPage + virtual bool ServesRequest(const infra::Tokenizer& pathTokens) const override; + virtual void RespondToRequest(services::HttpRequestParser& parser, services::HttpServerConnection& connection) override; + + // Implementation of HttpResponse + virtual infra::BoundedConstString Status() const override; + virtual void WriteBody(infra::TextOutputStream& stream) const override; + + private: + infra::BoundedConstString path; + services::ConfigurationStoreAccess hostname; + services::ConfigurationStoreAccess attributes; + services::ConfigurationStoreAccess password; + }; +} + +#endif diff --git a/postmaster/instantiations/HttpPageDiscovery.cpp b/postmaster/instantiations/HttpPageDiscovery.cpp new file mode 100644 index 0000000..6cf969b --- /dev/null +++ b/postmaster/instantiations/HttpPageDiscovery.cpp @@ -0,0 +1,45 @@ +#include "postmaster/instantiations/HttpPageDiscovery.hpp" +#include "infra/syntax/JsonFormatter.hpp" +#include "services/network/Address.hpp" +#include "services/network/HttpErrors.hpp" + +namespace application +{ + HttpPageDiscovery::HttpPageDiscovery(infra::BoundedConstString path, application::PostmasterDiscovery& discovery) + : path(path) + , discovery(discovery) + {} + + bool HttpPageDiscovery::ServesRequest(const infra::Tokenizer& pathTokens) const + { + return pathTokens.TokenAndRest(0) == path; + } + + void HttpPageDiscovery::RespondToRequest(services::HttpRequestParser& parser, services::HttpServerConnection& connection) + { + if (parser.Verb() == services::HttpVerb::get) + connection.SendResponse(*this); + else + connection.SendResponse(services::HttpResponseBadRequest::Instance()); + } + + infra::BoundedConstString HttpPageDiscovery::Status() const + { + return services::http_responses::ok; + } + + void HttpPageDiscovery::WriteBody(infra::TextOutputStream& stream) const + { + infra::JsonObjectFormatter discoveredDevices(stream); + auto devices = discoveredDevices.SubArray("devices"); + + discovery.EnumerateDiscoveredPostmasters([&devices](const PostmasterInfo& postmaster, infra::Duration seenSince) + { + auto device = devices.SubObject(); + + device.Add("hostname", postmaster.hostname); + device.Add("version", postmaster.versionFull); + device.Add("attributes", postmaster.attributes); + }); + } +} diff --git a/postmaster/instantiations/HttpPageDiscovery.hpp b/postmaster/instantiations/HttpPageDiscovery.hpp new file mode 100644 index 0000000..b2caf29 --- /dev/null +++ b/postmaster/instantiations/HttpPageDiscovery.hpp @@ -0,0 +1,30 @@ +#ifndef POSTMASTER_HTTP_PAGE_DISCOVERY_HPP +#define POSTMASTER_HTTP_PAGE_DISCOVERY_HPP + +#include "postmaster/instantiations/MdnsDiscovery.hpp" +#include "services/network/HttpServer.hpp" + +namespace application +{ + class HttpPageDiscovery + : public services::SimpleHttpPage + , public services::HttpResponse + { + public: + HttpPageDiscovery(infra::BoundedConstString path, application::PostmasterDiscovery& discovery); + + // Implementation of SimpleHttpPage + virtual bool ServesRequest(const infra::Tokenizer& pathTokens) const override; + virtual void RespondToRequest(services::HttpRequestParser& parser, services::HttpServerConnection& connection) override; + + // Implementation of HttpResponse + virtual infra::BoundedConstString Status() const override; + virtual void WriteBody(infra::TextOutputStream& stream) const override; + + private: + infra::BoundedConstString path; + application::PostmasterDiscovery& discovery; + }; +} + +#endif diff --git a/postmaster/instantiations/HttpServer.cpp b/postmaster/instantiations/HttpServer.cpp new file mode 100644 index 0000000..ad9fc29 --- /dev/null +++ b/postmaster/instantiations/HttpServer.cpp @@ -0,0 +1,30 @@ +#include "postmaster/instantiations/HttpServer.hpp" + +namespace main_ +{ + HttpServer::HttpServer(services::ConnectionFactory& connectionFactory, services::ConfigurationStoreAccess& hostname, + services::ConfigurationStoreAccess& attributes, services::ConfigurationStoreAccess& password, + application::Authentication& authentication, application::PostmasterDiscovery& postmasterDiscovery, + UartCreator& uartProgrammerCreator, UartCreator& uartExternalCreator, hal::Flash& upgradeFlash, hal::Reset& reset, const infra::Function& reporter, const infra::Function& receivingTarget, const infra::Function& receivingSelf, hal::GpioPin& resetTarget, hal::GpioPin& boot0) + : HttpServerFrontEnd(connectionFactory, hostname, attributes, password, authentication, reporter, receivingTarget, receivingSelf) + , discovery("discovery", postmasterDiscovery) + , uartProgrammerCreator(uartProgrammerCreator) + , uartExternalCreator(uartExternalCreator) + , selfFirmwarePage{ authentication, "firmware/self" } + , selfProgrammer{ selfFirmwarePage.page, upgradeFlash, reset, receivingSelf } + , targetFirmwarePage{ authentication, "firmware/target" } + , targetProgrammer{ uartProgrammerCreator, targetFirmwarePage.page, resetTarget, boot0, receivingTarget } + , targetUartProgrammer{ authentication, "target/uart/programmer", uartProgrammerCreator } + , targetUartExternal{ authentication, "target/uart/external", uartExternalCreator } + , targetUartEchoProgrammer{ authentication, "target/echo/programmer", uartProgrammerCreator } + , targetUartEchoExternal{ authentication, "target/echo/external", uartExternalCreator } + { + server.AddPage(selfFirmwarePage); + server.AddPage(targetFirmwarePage); + server.AddPage(targetUartProgrammer.page); + server.AddPage(targetUartExternal.page); + server.AddPage(targetUartEchoProgrammer.page); + server.AddPage(targetUartEchoExternal.page); + server.AddPage(discovery); + } +} diff --git a/postmaster/instantiations/HttpServer.hpp b/postmaster/instantiations/HttpServer.hpp new file mode 100644 index 0000000..562d963 --- /dev/null +++ b/postmaster/instantiations/HttpServer.hpp @@ -0,0 +1,39 @@ +#ifndef POSTMASTER_HTTP_SERVER_HPP +#define POSTMASTER_HTTP_SERVER_HPP + +#include "postmaster/instantiations/HttpPageDiscovery.hpp" +#include "postmaster/instantiations/HttpServerFrontEnd.hpp" +#include "postmaster/instantiations/SelfProgrammer.hpp" +#include "postmaster/instantiations/TargetProgrammer.hpp" +#include "postmaster/instantiations/TargetUart.hpp" +#include "postmaster/instantiations/TargetUartEcho.hpp" + +namespace main_ +{ + struct HttpServer + : public HttpServerFrontEnd + { + HttpServer(services::ConnectionFactory& connectionFactory, services::ConfigurationStoreAccess& hostname, + services::ConfigurationStoreAccess& attributes, services::ConfigurationStoreAccess& password, + application::Authentication& authentication, application::PostmasterDiscovery& postmasterDiscovery, + UartCreator& uartProgrammerCreator, UartCreator& uartExternalCreator, hal::Flash& upgradeFlash, hal::Reset& reset, const infra::Function& reporter, const infra::Function& receivingTarget, const infra::Function& receivingSelf, hal::GpioPin& resetTarget, hal::GpioPin& boot0); + + UartCreator& uartProgrammerCreator; + UartCreator& uartExternalCreator; + + application::HttpPageDiscovery discovery; + application::AuthenticatedHttpPage::WithPage selfFirmwarePage; + main_::SelfProgrammer selfProgrammer; + + application::AuthenticatedHttpPage::WithPage targetFirmwarePage; + main_::TargetProgrammer targetProgrammer; + + main_::TargetUart targetUartProgrammer; + main_::TargetUart targetUartExternal; + + main_::TargetUartEcho targetUartEchoProgrammer; + main_::TargetUartEcho targetUartEchoExternal; + }; +} + +#endif diff --git a/postmaster/instantiations/HttpServerFrontEnd.cpp b/postmaster/instantiations/HttpServerFrontEnd.cpp new file mode 100644 index 0000000..d18f158 --- /dev/null +++ b/postmaster/instantiations/HttpServerFrontEnd.cpp @@ -0,0 +1,17 @@ +#include "postmaster/instantiations/HttpServer.hpp" + +namespace main_ +{ + HttpServerFrontEnd::HttpServerFrontEnd(services::ConnectionFactory& connectionFactory, services::ConfigurationStoreAccess& hostname, + services::ConfigurationStoreAccess& attributes, services::ConfigurationStoreAccess& password, application::Authentication& authentication, + const infra::Function& reporter, const infra::Function& receivingTarget, + const infra::Function& receivingSelf) + : configuration{ authentication, "configuration", hostname, attributes, password } + , server{ connectionFactory, 80, reporter } + { + server.AddPage(frontendHtml); + server.AddPage(frontendCss); + server.AddPage(frontendJs); + server.AddPage(configuration); + } +} diff --git a/postmaster/instantiations/HttpServerFrontEnd.hpp b/postmaster/instantiations/HttpServerFrontEnd.hpp new file mode 100644 index 0000000..d40843b --- /dev/null +++ b/postmaster/instantiations/HttpServerFrontEnd.hpp @@ -0,0 +1,36 @@ +#ifndef POSTMASTER_HTTP_SERVER_FRONT_END_HPP +#define POSTMASTER_HTTP_SERVER_FRONT_END_HPP + +#include "generated/page_contents/IndexHtml.hpp" +#include "generated/page_contents/StyleCss.hpp" +#include "generated/page_contents/UploadJs.hpp" +#include "postmaster/frontend/AuthenticatedHttpPage.hpp" +#include "postmaster/frontend/ReportingHttpServer.hpp" +#include "postmaster/instantiations/HttpPageConfiguration.hpp" +#include "services/network/HttpServer.hpp" +#include "services/util/ConfigurationStore.hpp" + +namespace main_ +{ + struct HttpServerFrontEnd + : public services::Stoppable + { + HttpServerFrontEnd(services::ConnectionFactory& connectionFactory, services::ConfigurationStoreAccess& hostname, + services::ConfigurationStoreAccess& attributes, services::ConfigurationStoreAccess& password, + application::Authentication& authentication, const infra::Function& reporter, const infra::Function& receivingTarget, const infra::Function& receivingSelf); + + void Stop(const infra::Function& onDone) override + { + server.Stop(onDone); + } + + services::HttpPageWithContent frontendHtml{ "", page_contents::indexHtml, "text/html;charset=UTF-8" }; + services::HttpPageWithContent frontendCss{ "style.css", page_contents::styleCss, "text/css" }; + services::HttpPageWithContent frontendJs{ "upload.js", page_contents::uploadJs, "text/javascript" }; + application::AuthenticatedHttpPage::WithPage configuration; + + application::ReportingHttpServer::WithBuffer<8192> server; + }; +} + +#endif diff --git a/postmaster/instantiations/Mdns.cpp b/postmaster/instantiations/Mdns.cpp new file mode 100644 index 0000000..339eca8 --- /dev/null +++ b/postmaster/instantiations/Mdns.cpp @@ -0,0 +1,21 @@ +#include "postmaster/instantiations/Mdns.hpp" + +namespace main_ +{ + Mdns::Mdns(infra::BoundedConstString hostname, services::DatagramFactory& factory, services::Multicast& multicast, services::IPv4Address ipAddress, infra::BoundedConstString version, infra::BoundedConstString versionFull, infra::BoundedConstString attributes) + : name{ Create<35>(hostname, "nm") } + , version{ Create<16>(version, "vs") } + , versionFull{ Create<19>(versionFull, "vf") } + , attributes{ Create<67>(attributes, "at") } + , server(factory, multicast, hostname, "postmaster", "_tcp", infra::MakeOptional(ipAddress), infra::none, 80, text) + {} + + template + infra::BoundedString::WithStorage Mdns::Create(infra::BoundedConstString contents, infra::BoundedConstString prefix) const + { + infra::BoundedString::WithStorage result{ prefix }; + result.push_back(':'); + result += contents; + return result; + } +} diff --git a/postmaster/instantiations/Mdns.hpp b/postmaster/instantiations/Mdns.hpp new file mode 100644 index 0000000..a2bd31d --- /dev/null +++ b/postmaster/instantiations/Mdns.hpp @@ -0,0 +1,25 @@ +#ifndef POSTMASTER_MDNS_HPP +#define POSTMASTER_MDNS_HPP + +#include "services/network/BonjourServer.hpp" + +namespace main_ +{ + struct Mdns + { + Mdns(infra::BoundedConstString hostname, services::DatagramFactory& factory, services::Multicast& multicast, + services::IPv4Address ipAddress, infra::BoundedConstString version, infra::BoundedConstString versionFull, infra::BoundedConstString attributes); + + template + infra::BoundedString::WithStorage Create(infra::BoundedConstString contents, infra::BoundedConstString prefix) const; + + infra::BoundedString::WithStorage<35> name; + infra::BoundedString::WithStorage<16> version; + infra::BoundedString::WithStorage<19> versionFull; + infra::BoundedString::WithStorage<67> attributes; + services::DnsHostnameInPartsHelper<4> text{ services::DnsHostnameInParts(name)(version)(versionFull)(attributes) }; + services::BonjourServer server; + }; +} + +#endif diff --git a/postmaster/instantiations/MdnsDiscovery.cpp b/postmaster/instantiations/MdnsDiscovery.cpp new file mode 100644 index 0000000..e21cd19 --- /dev/null +++ b/postmaster/instantiations/MdnsDiscovery.cpp @@ -0,0 +1,178 @@ +#include "postmaster/instantiations/MdnsDiscovery.hpp" +#include "infra/stream/ByteInputStream.hpp" + +namespace application +{ + namespace + { + bool StringEndsIn(infra::BoundedConstString string, infra::BoundedConstString end) + { + return string.size() >= end.size() && string.substr(string.size() - end.size()) == end; + } + } + + PostmasterDiscovery::PostmasterDiscovery(infra::BoundedVector>& postmasters, MdnsQueryPostmaster& mdnsQuery) + : infra::Observer(mdnsQuery) + , postmasters(postmasters) + {} + + void PostmasterDiscovery::Discovered(const PostmasterInfo& postmaster) + { + for (auto& i : postmasters) + if (i.first.hostname == postmaster.hostname) + { + i.first = postmaster; + i.second = infra::Now(); + return; + } + + if (postmasters.full()) + RemoveOldestPostmaster(); + + postmasters.push_back({ postmaster, infra::Now() }); + } + + void PostmasterDiscovery::EnumerateDiscoveredPostmasters(const infra::Function& enumerator) + { + auto now = infra::Now(); + + for (const auto& postmaster : postmasters) + enumerator(postmaster.first, now - postmaster.second); + } + + void PostmasterDiscovery::Refresh() + { + RemoveExpiredPostmasters(); + + bool necessary = true; + auto minimumSeen = infra::Now() - refreshTimer.TriggerPeriod(); + + for (const auto& i : postmasters) + necessary = necessary && i.second > minimumSeen; + + if (necessary) + Subject().Query(); + } + + void PostmasterDiscovery::RemoveOldestPostmaster() + { + assert(!postmasters.empty()); + auto minimum = postmasters.begin(); + + auto i = postmasters.begin(); + for (; i != postmasters.end(); ++i) + if (i->second < minimum->second) + minimum = i; + + postmasters.erase(minimum); + } + + void PostmasterDiscovery::RemoveExpiredPostmasters() + { + auto minimumSeen = infra::Now() - 2 * refreshTimer.TriggerPeriod(); + infra::erase_if(postmasters, [minimumSeen](auto& info) + { + return info.second <= minimumSeen; + }); + } + + MdnsQueryPostmaster::MdnsQueryPostmaster(services::MdnsClient& mdnsClient) + : mdnsClient(mdnsClient) + { + mdnsClient.RegisterQuery(*this); + } + + MdnsQueryPostmaster::~MdnsQueryPostmaster() + { + mdnsClient.UnRegisterQuery(*this); + } + + void MdnsQueryPostmaster::Query() + { + mdnsClient.ActiveQuerySingleShot(*this); + } + + infra::BoundedConstString MdnsQueryPostmaster::DnsHostname() const + { + return "postmaster._tcp.local"; + } + + services::DnsType MdnsQueryPostmaster::DnsType() const + { + return services::DnsType::dnsTypePtr; + } + + bool MdnsQueryPostmaster::IsWaiting() const + { + return waiting; + } + + void MdnsQueryPostmaster::SetWaiting(bool waiting) + { + this->waiting = waiting; + } + + services::IPVersions MdnsQueryPostmaster::IpVersion() const + { + return services::IPVersions::ipv4; + } + + void MdnsQueryPostmaster::CheckAnswer(services::IPVersions ipVersion, infra::BoundedString& hostname, services::DnsRecordPayload& payload, infra::ConstByteRange data) + { + auto type = static_cast(static_cast(payload.type)); + + if (type == services::DnsType::dnsTypeA) + { + infra::ByteInputStream stream(data, infra::noFail); + info.ipAddress = stream.Extract(); + } + + if ((type == services::DnsType::dnsTypeTxt) && StringEndsIn(hostname, ".postmaster._tcp.local")) + { + valid = true; + info.hostname = hostname.substr(0, info.hostname.max_size()); + ParseData(data); + } + } + + void MdnsQueryPostmaster::CheckAdditionalRecord(infra::BoundedString& hostname, services::DnsRecordPayload& payload, infra::ConstByteRange data) + { + CheckAnswer(services::IPVersions::ipv4, hostname, payload, data); + } + + void MdnsQueryPostmaster::EndOfAnswerNotification() + { + if (valid) + NotifyObservers([this](auto& observer) + { + observer.Discovered(info); + }); + + valid = false; + infra::ReConstruct(info); + } + + void MdnsQueryPostmaster::ParseData(infra::ConstByteRange data) + { + while (!data.empty()) + { + uint8_t length = data.front(); + data.pop_front(); + + ParseField(infra::ByteRangeAsString(infra::Head(data, length))); + data.pop_front(length); + } + } + + void MdnsQueryPostmaster::ParseField(infra::BoundedConstString data) + { + infra::Tokenizer tokenizer(data, ':'); + + if (tokenizer.Token(0) == "vs") + info.version = tokenizer.TokenAndRest(1).substr(0, info.version.max_size()); + else if (tokenizer.Token(0) == "vf") + info.versionFull = tokenizer.TokenAndRest(1).substr(0, info.versionFull.max_size()); + else if (tokenizer.Token(0) == "at") + info.attributes = tokenizer.TokenAndRest(1).substr(0, info.attributes.max_size()); + } +} diff --git a/postmaster/instantiations/MdnsDiscovery.hpp b/postmaster/instantiations/MdnsDiscovery.hpp new file mode 100644 index 0000000..accda8e --- /dev/null +++ b/postmaster/instantiations/MdnsDiscovery.hpp @@ -0,0 +1,94 @@ +#ifndef POSTMASTER_MDNS_DISCOVERY_HPP +#define POSTMASTER_MDNS_DISCOVERY_HPP + +#include "infra/timer/Timer.hpp" +#include "infra/util/BoundedVector.hpp" +#include "services/network/MdnsClient.hpp" + +namespace application +{ + struct PostmasterInfo + { + services::IPv4Address ipAddress; + infra::BoundedString::WithStorage<64> hostname; + infra::BoundedString::WithStorage<16> version; + infra::BoundedString::WithStorage<16> versionFull; + infra::BoundedString::WithStorage<64> attributes; + }; + + class MdnsQueryPostmaster; + + class PostmasterDiscovery + : public infra::Observer + { + public: + template + using WithStorage = infra::WithStorage>::WithMaxSize>; + + PostmasterDiscovery(infra::BoundedVector>& postmasters, MdnsQueryPostmaster& mdnsQuery); + + void Discovered(const PostmasterInfo& postmaster); + void EnumerateDiscoveredPostmasters(const infra::Function& enumerator); + + private: + void Refresh(); + void RemoveOldestPostmaster(); + void RemoveExpiredPostmasters(); + + private: + infra::BoundedVector>& postmasters; + infra::TimerRepeating refreshTimer{ std::chrono::seconds(5), [this]() + { + Refresh(); + } }; + }; + + class MdnsQueryPostmaster + : public infra::Subject + , private services::MdnsQuery + { + public: + MdnsQueryPostmaster(services::MdnsClient& mdnsClient); + ~MdnsQueryPostmaster(); + + void Query(); + + private: + // Implementation of MdnsQuery + virtual infra::BoundedConstString DnsHostname() const override; + virtual services::DnsType DnsType() const override; + virtual bool IsWaiting() const override; + virtual void SetWaiting(bool waiting) override; + virtual services::IPVersions IpVersion() const override; + virtual void CheckAnswer(services::IPVersions ipVersion, infra::BoundedString& hostname, services::DnsRecordPayload& payload, infra::ConstByteRange data) override; + virtual void CheckAdditionalRecord(infra::BoundedString& hostname, services::DnsRecordPayload& payload, infra::ConstByteRange data) override; + virtual void EndOfAnswerNotification() override; + + private: + void ParseData(infra::ConstByteRange data); + void ParseField(infra::BoundedConstString data); + + private: + services::MdnsClient& mdnsClient; + + bool waiting = false; + bool valid = false; + PostmasterInfo info; + }; +} + +namespace main_ +{ + struct MdnsDiscovery + { + MdnsDiscovery(services::DatagramFactory& datagramFactory, services::Multicast& multicast) + : mdnsClient(datagramFactory, multicast, services::IPVersions::ipv4) + {} + + services::MdnsClient mdnsClient; + application::MdnsQueryPostmaster postmasterQuery{ mdnsClient }; + application::PostmasterDiscovery::WithStorage<100> discovery{ postmasterQuery }; + }; +} + +#endif diff --git a/postmaster/instantiations/PostmasterConfiguration.proto b/postmaster/instantiations/PostmasterConfiguration.proto new file mode 100644 index 0000000..c7f6090 --- /dev/null +++ b/postmaster/instantiations/PostmasterConfiguration.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +import "EchoAttributes.proto"; + +package postmaster_configuration; + +message Configuration +{ + string hostname = 1 [(string_size) = 64]; + string attributes = 2 [(string_size) = 64]; + string password = 3 [(string_size) = 64]; +} diff --git a/postmaster/instantiations/SelfProgrammer.hpp b/postmaster/instantiations/SelfProgrammer.hpp new file mode 100644 index 0000000..70df345 --- /dev/null +++ b/postmaster/instantiations/SelfProgrammer.hpp @@ -0,0 +1,27 @@ +#ifndef POSTMASTER_SELF_PROGRAMMER_HPP +#define POSTMASTER_SELF_PROGRAMMER_HPP + +#include "postmaster/programmer/FirmwareReceptorReporting.hpp" +#include "postmaster/programmer/FirmwareReceptorResetWhenDone.hpp" +#include "postmaster/programmer/FirmwareReceptorToFlash.hpp" +#include "postmaster/programmer/HttpPageFirmware.hpp" + +namespace main_ +{ + struct SelfProgrammer + { + SelfProgrammer(application::HttpPageFirmware& page, hal::Flash& flash, hal::Reset& reset, const infra::Function& receiving) + : firmwareReceptor{ flash } + , firmwareReceptorReboot{ firmwareReceptor, reset } + , firmwareReceptorReporting(firmwareReceptorReboot, receiving) + { + page.SetReceptor(firmwareReceptorReporting); + } + + application::FirmwareReceptorToFlash firmwareReceptor; + application::FirmwareReceptorResetWhenDone firmwareReceptorReboot; + application::FirmwareReceptorReporting firmwareReceptorReporting; + }; +} + +#endif diff --git a/postmaster/instantiations/TargetProgrammer.hpp b/postmaster/instantiations/TargetProgrammer.hpp new file mode 100644 index 0000000..9f8aa2b --- /dev/null +++ b/postmaster/instantiations/TargetProgrammer.hpp @@ -0,0 +1,76 @@ +#ifndef POSTMASTER_TARGET_PROGRAMMER_HPP +#define POSTMASTER_TARGET_PROGRAMMER_HPP + +#include "infra/util/Function.hpp" +#include "postmaster/instantiations/UartCreator.hpp" +#include "postmaster/programmer/FirmwareReceptorReporting.hpp" +#include "postmaster/programmer/FirmwareReceptorResetTarget.hpp" +#include "postmaster/programmer/FirmwareReceptorToFlash.hpp" +#include "postmaster/programmer/HttpPageFirmware.hpp" +#include "services/st_util/FlashOnStBootloaderCommunicator.hpp" +#include "services/st_util/StBootloaderCommunicatorUart.hpp" + +namespace main_ +{ + struct TargetProgrammer + { + TargetProgrammer(UartCreator& uartProgrammerCreator, application::HttpPageFirmware& page, hal::GpioPin& reset, hal::GpioPin& boot0, const infra::Function& receiving) + : uartProgrammerCreator(uartProgrammerCreator) + , page(page) + , receptorResetTarget(reset, boot0, stateCreator) + , firmwareReceptorReporting(receptorResetTarget, receiving) + , stateCreator([this]() -> application::FirmwareReceptor& + { + state.Emplace(this->uartProgrammerCreator, infra::emptyFunction, [this](infra::BoundedConstString reason) + { + this->page.Close(); + this->page.ResetReceptor(reason); + this->page.SetReceptor(firmwareReceptorReporting); + }); + return state->firmwareReceptorToFlash; + }, + [this]() + { + state = infra::none; + }) + { + this->page.SetReceptor(firmwareReceptorReporting); + } + + ~TargetProgrammer() + { + page.ResetReceptor("Destructing"); + } + + struct State + { + State(UartCreator& uartProgrammerCreator, const infra::Function& onInitialized, const infra::Function& onError) + : serial{ uartProgrammerCreator, []() + { + UartConfig config; + config.baudrate = 57600; + config.parity = UartConfig::Parity::even; + return config; + }() } + , communicator(*serial, onInitialized, onError) + {} + + infra::ProxyCreator serial; + + services::StBootloaderCommunicatorUart communicator; + std::array sectors{ 0x8000, 0x8000, 0x8000, 0x8000, 0x20000, 0x40000, 0x40000, 0x40000, 0x40000, 0x40000, 0x40000, 0x40000 }; + services::FlashHeterogeneousOnStBootloaderCommunicator communicatorFlash{ sectors, communicator }; + application::FirmwareReceptorToFlash firmwareReceptorToFlash{ communicatorFlash }; + }; + + UartCreator& uartProgrammerCreator; + application::HttpPageFirmware& page; + + application::FirmwareReceptorResetTarget receptorResetTarget; + application::FirmwareReceptorReporting firmwareReceptorReporting; + infra::Optional state; + infra::CreatorExternal stateCreator; + }; +} + +#endif diff --git a/postmaster/instantiations/TargetUart.cpp b/postmaster/instantiations/TargetUart.cpp new file mode 100644 index 0000000..d26ee40 --- /dev/null +++ b/postmaster/instantiations/TargetUart.cpp @@ -0,0 +1,15 @@ +#include "postmaster/instantiations/TargetUart.hpp" + +namespace main_ +{ + TargetUart::TargetUart(application::Authentication& authentication, infra::BoundedConstString url, UartCreator& serialCreator) + : serialCreator(serialCreator) + , targetUartConnection([this](infra::Optional>& value) + { + value.Emplace(); + auto bridgePtr = bridge.Emplace(this->serialCreator); + value->services::Connection::Attach(infra::MakeContainedSharedObject(bridgePtr->uartConnection, bridgePtr)); + }) + , page{ authentication, url, factory } + {} +} diff --git a/postmaster/instantiations/TargetUart.hpp b/postmaster/instantiations/TargetUart.hpp new file mode 100644 index 0000000..06fbf1d --- /dev/null +++ b/postmaster/instantiations/TargetUart.hpp @@ -0,0 +1,34 @@ +#ifndef POSTMASTER_TARGET_UART_HPP +#define POSTMASTER_TARGET_UART_HPP + +#include "postmaster/frontend/AuthenticatedHttpPage.hpp" +#include "postmaster/instantiations/UartCreator.hpp" +#include "postmaster/programmer/TargetUartConnection.hpp" +#include "services/network/HttpPageWebSocket.hpp" +#include "services/network/WebSocketServerConnectionObserver.hpp" + +namespace main_ +{ + struct TargetUart + { + TargetUart(application::Authentication& authentication, infra::BoundedConstString url, UartCreator& serialCreator); + + struct Bridge + { + Bridge(UartCreator& serialCreator) + : serial(serialCreator, UartConfig()) + {} + + infra::ProxyCreator serial; + application::TargetUartConnection::WithStorage<4096> uartConnection{ *serial }; + }; + + UartCreator& serialCreator; + infra::SharedOptional bridge; + infra::Creator, void()> targetUartConnection; + services::WebSocketObserverFactoryImpl factory{ { targetUartConnection } }; + application::AuthenticatedHttpPage::WithPage page; + }; +} + +#endif diff --git a/postmaster/instantiations/TargetUartEcho.cpp b/postmaster/instantiations/TargetUartEcho.cpp new file mode 100644 index 0000000..e689ea5 --- /dev/null +++ b/postmaster/instantiations/TargetUartEcho.cpp @@ -0,0 +1,27 @@ +#include "postmaster/instantiations/TargetUartEcho.hpp" + +namespace main_ +{ + TargetUartEcho::TargetUartEcho(application::Authentication& authentication, infra::BoundedConstString url, UartCreator& serialCreator) + : serialCreator(serialCreator) + , targetUartConnectionCreator([this](infra::Optional& value) + { + value.Emplace(this->serialCreator); + }) + , page{ authentication, url, factory } + {} + + TargetUartEcho::Bridge::Bridge(UartCreator& serialCreator) + : serial(serialCreator, UartConfig()) + {} + + TargetUartEcho::Bridge::operator services::ConnectionObserver&() + { + return webSocket; + } + + TargetUartEcho::Bridge::operator const services::ConnectionObserver&() const + { + return webSocket; + } +} diff --git a/postmaster/instantiations/TargetUartEcho.hpp b/postmaster/instantiations/TargetUartEcho.hpp new file mode 100644 index 0000000..13e453c --- /dev/null +++ b/postmaster/instantiations/TargetUartEcho.hpp @@ -0,0 +1,63 @@ +#ifndef POSTMASTER_TARGET_UART_ECHO_HPP +#define POSTMASTER_TARGET_UART_ECHO_HPP + +#include "hal/interfaces/SerialCommunication.hpp" +#include "infra/event/QueueForOneReaderOneIrqWriter.hpp" +#include "infra/util/BoundedDeque.hpp" +#include "postmaster/frontend/AuthenticatedHttpPage.hpp" +#include "postmaster/instantiations/UartCreator.hpp" +#include "services/network/EchoOnConnection.hpp" +#include "services/network/HttpPageWebSocket.hpp" +#include "services/network/WebSocketServerConnectionObserver.hpp" +#include "services/util/EchoInstantiation.hpp" + +namespace main_ +{ + struct TargetUartEcho + { + TargetUartEcho(application::Authentication& authentication, infra::BoundedConstString url, UartCreator& serialCreator); + + struct Bridge + { + Bridge(UartCreator& serialCreator); + + class WebSocketServerConnectionObserver + : public services::WebSocketServerConnectionObserver::WithBufferSizes<1500, 1500> + { + public: + WebSocketServerConnectionObserver(Bridge& bridge) + : bridge(bridge) + {} + + protected: + void Attached() override + { + services::Connection::Attach(infra::MakeContainedSharedObject(bridge.echoConnection, ObserverPtr())); + } + + private: + Bridge& bridge; + }; + + infra::ProxyCreator serial; + services::MethodSerializerFactory::ForServices<>::AndProxies<> serializerFactory; + hal::BufferedSerialCommunicationOnUnbuffered::WithStorage<256> bufferedSerial{ *serial }; + main_::EchoOnSesame<256> echoUart{ bufferedSerial, serializerFactory }; + services::EchoOnConnection echoConnection{ serializerFactory }; + WebSocketServerConnectionObserver webSocket{ *this }; + + services::ServiceForwarderAll forwardLeft{ echoUart, echoConnection }; + services::ServiceForwarderAll forwardRight{ echoConnection, echoUart }; + + operator services::ConnectionObserver&(); + operator const services::ConnectionObserver&() const; + }; + + UartCreator& serialCreator; + infra::Creator targetUartConnectionCreator; + services::WebSocketObserverFactoryImpl factory{ { targetUartConnectionCreator } }; + application::AuthenticatedHttpPage::WithPage page; + }; +} + +#endif diff --git a/postmaster/instantiations/UartCreator.hpp b/postmaster/instantiations/UartCreator.hpp new file mode 100644 index 0000000..d64a5eb --- /dev/null +++ b/postmaster/instantiations/UartCreator.hpp @@ -0,0 +1,27 @@ +#ifndef POSTMASTER_UART_CONFIG_HPP +#define POSTMASTER_UART_CONFIG_HPP + +#include "hal/interfaces/SerialCommunication.hpp" +#include "infra/util/ProxyCreator.hpp" + +namespace main_ +{ + struct UartConfig + { + constexpr UartConfig() + {} + + enum class Parity + { + none, + even + }; + + uint32_t baudrate = 115200; + Parity parity = Parity::none; + }; + + using UartCreator = infra::CreatorBase; +} + +#endif diff --git a/postmaster/instantiations/ViewNetworkStatus.cpp b/postmaster/instantiations/ViewNetworkStatus.cpp new file mode 100644 index 0000000..23e4806 --- /dev/null +++ b/postmaster/instantiations/ViewNetworkStatus.cpp @@ -0,0 +1,47 @@ +#include "postmaster/instantiations/ViewNetworkStatus.hpp" +#include "infra/stream/StringOutputStream.hpp" +#include "lwip/netif.h" + +namespace application +{ + ViewNetworkStatus::ViewNetworkStatus() + : services::ViewText(services::TextAttributes{ infra::Colour::white, infra::freeSans9pt7b }) + {} + + void ViewNetworkStatus::Update() + { + status.clear(); + infra::StringOutputStream stream(status); + + if (netif_is_link_up(netif_default)) + stream << ip4_addr1(&netif_default->ip_addr.u_addr.ip4) << "." << ip4_addr2(&netif_default->ip_addr.u_addr.ip4) << "." << ip4_addr3(&netif_default->ip_addr.u_addr.ip4) << "." << ip4_addr4(&netif_default->ip_addr.u_addr.ip4); + else + stream << "Link down"; + + SetTextAndResize(status); + } + + ViewNetworkQrCode::ViewNetworkQrCode(const infra::BoundedString& password) + : services::QrCode<3, services::QrCodeEcc::medium>{ "No connection" } + , services::ViewBitmap{ static_cast(*this) } + , password(password) + {} + + void ViewNetworkQrCode::UpdateNetworkStatus() + { + status.clear(); + infra::StringOutputStream stream(status, infra::noFail); + + if (netif_is_link_up(netif_default)) + { + stream << "http://" << ip4_addr1(&netif_default->ip_addr.u_addr.ip4) << "." << ip4_addr2(&netif_default->ip_addr.u_addr.ip4) << "." << ip4_addr3(&netif_default->ip_addr.u_addr.ip4) << "." << ip4_addr4(&netif_default->ip_addr.u_addr.ip4); + if (!password.empty()) + stream << "?p=" << password; + } + else + stream << "No connection"; + + Update(status); + Source(static_cast(*this)); + } +}; diff --git a/postmaster/instantiations/ViewNetworkStatus.hpp b/postmaster/instantiations/ViewNetworkStatus.hpp new file mode 100644 index 0000000..5951153 --- /dev/null +++ b/postmaster/instantiations/ViewNetworkStatus.hpp @@ -0,0 +1,51 @@ +#ifndef POSTMASTER_VIEW_NETWORK_STATUS_HPP +#define POSTMASTER_VIEW_NETWORK_STATUS_HPP + +#include "infra/timer/Timer.hpp" +#include "preview/interfaces/QrCode.hpp" +#include "preview/views/ViewBitmap.hpp" +#include "preview/views/ViewText.hpp" +#include "services/util/ConfigurationStore.hpp" + +namespace application +{ + class ViewNetworkStatus + : public services::ViewText + { + public: + ViewNetworkStatus(); + + private: + void Update(); + + private: + infra::BoundedString::WithStorage<32> status; + infra::TimerRepeating update{ std::chrono::milliseconds(250), [this]() + { + Update(); + }, + infra::triggerImmediately }; + }; + + class ViewNetworkQrCode + : private services::QrCode<3, services::QrCodeEcc::medium> + , public services::ViewBitmap + { + public: + ViewNetworkQrCode(const infra::BoundedString& password); + + private: + void UpdateNetworkStatus(); + + private: + const infra::BoundedString& password; + infra::BoundedString::WithStorage status; + infra::TimerRepeating update{ std::chrono::seconds(1), [this]() + { + UpdateNetworkStatus(); + }, + infra::triggerImmediately }; + }; +}; + +#endif diff --git a/postmaster/instantiations/ViewStatus.cpp b/postmaster/instantiations/ViewStatus.cpp new file mode 100644 index 0000000..1c0fffa --- /dev/null +++ b/postmaster/instantiations/ViewStatus.cpp @@ -0,0 +1,51 @@ +#include "postmaster/instantiations/ViewStatus.hpp" +#include "infra/stream/StringOutputStream.hpp" + +namespace application +{ + ViewStatus::ViewStatus(infra::BoundedConstString hostname) + : services::ViewText(services::TextAttributes{ infra::Colour::white, infra::freeSans9pt7b }) + , hostname(hostname) + { + Update(); + } + + void ViewStatus::SetConnectionOpen(bool open, services::IPAddress newAddress) + { + connectionOpen = open; + address = newAddress; + + Update(); + } + + void ViewStatus::SetReceivingTarget(bool receiving) + { + receivingTarget = receiving; + + Update(); + } + + void ViewStatus::SetReceivingSelf(bool receiving) + { + receivingSelf = receiving; + + Update(); + } + + void ViewStatus::Update() + { + status.clear(); + infra::StringOutputStream stream(status); + + if (!connectionOpen) + stream << hostname << " idle"; + else if (receivingTarget) + stream << hostname << " receiving target firmware from " << address; + else if (receivingSelf) + stream << hostname << " receiving upgrade from " << address; + else + stream << hostname << " connected to " << address; + + SetTextAndResize(status); + } +} diff --git a/postmaster/instantiations/ViewStatus.hpp b/postmaster/instantiations/ViewStatus.hpp new file mode 100644 index 0000000..ace9c99 --- /dev/null +++ b/postmaster/instantiations/ViewStatus.hpp @@ -0,0 +1,35 @@ +#ifndef POSTMASTER_VIEW_STATUS_HPP +#define POSTMASTER_VIEW_STATUS_HPP + +#include "infra/util/BoundedString.hpp" +#include "preview/views/ViewText.hpp" +#include "services/network/Address.hpp" + +namespace application +{ + class ViewStatus + : public services::ViewText + { + public: + ViewStatus(infra::BoundedConstString hostname); + + void SetConnectionOpen(bool open, services::IPAddress newAddress); + void SetReceivingTarget(bool receiving); + void SetReceivingSelf(bool receiving); + + private: + void Update(); + + private: + infra::BoundedConstString hostname; + + bool connectionOpen = false; + services::IPAddress address; + bool receivingTarget = false; + bool receivingSelf = false; + + infra::BoundedString::WithStorage<128> status; + }; +} + +#endif diff --git a/postmaster/instantiations_st/CMakeLists.txt b/postmaster/instantiations_st/CMakeLists.txt new file mode 100644 index 0000000..a3f44b6 --- /dev/null +++ b/postmaster/instantiations_st/CMakeLists.txt @@ -0,0 +1,25 @@ +add_library(postmaster.instantiations_st STATIC) +emil_build_for(postmaster.instantiations_st TARGET_MCU_VENDOR st) + +target_include_directories(postmaster.instantiations_st PUBLIC + "$" + "$" +) + +target_sources(postmaster.instantiations_st PRIVATE + ConvertUart.hpp + ConvertUart.cpp + Display.cpp + Display.hpp + EthernetPostmaster.hpp + Flash.cpp + Flash.hpp + HttpServerSt.cpp + HttpServerSt.hpp + NetworkConnected.hpp +) + +target_link_libraries(postmaster.instantiations_st PUBLIC + postmaster.instantiations + hal_st.stm32fxxx +) diff --git a/postmaster/instantiations_st/ConvertUart.cpp b/postmaster/instantiations_st/ConvertUart.cpp new file mode 100644 index 0000000..d9c71c8 --- /dev/null +++ b/postmaster/instantiations_st/ConvertUart.cpp @@ -0,0 +1,27 @@ +#include "postmaster/instantiations_st/ConvertUart.hpp" + +namespace main_ +{ + uint32_t Convert(UartConfig::Parity parity) + { + switch (parity) + { + case UartConfig::Parity::none: + return USART_PARITY_NONE; + case UartConfig::Parity::even: + return USART_PARITY_EVEN; + default: + std::abort(); + } + } + + hal::UartStmDma::Config Convert(const UartConfig& config) + { + hal::UartStmDma::Config result; + + result.baudrate = config.baudrate; + result.parity = Convert(config.parity); + + return result; + } +} diff --git a/postmaster/instantiations_st/ConvertUart.hpp b/postmaster/instantiations_st/ConvertUart.hpp new file mode 100644 index 0000000..bec21b6 --- /dev/null +++ b/postmaster/instantiations_st/ConvertUart.hpp @@ -0,0 +1,13 @@ +#ifndef POSTMASTER_CONVERT_UART_HPP +#define POSTMASTER_CONVERT_UART_HPP + +#include "hal_st/stm32fxxx/UartStmDma.hpp" +#include "postmaster/instantiations/TargetProgrammer.hpp" + +namespace main_ +{ + uint32_t Convert(UartConfig::Parity parity); + hal::UartStmDma::Config Convert(const UartConfig& config); +} + +#endif diff --git a/postmaster/instantiations_st/Display.cpp b/postmaster/instantiations_st/Display.cpp new file mode 100644 index 0000000..dac5e89 --- /dev/null +++ b/postmaster/instantiations_st/Display.cpp @@ -0,0 +1,17 @@ +#include "postmaster/instantiations_st/Display.hpp" + +namespace main_ +{ + Display::Display(const postmaster_configuration::Configuration& configuration) + : viewStatus{ std::chrono::milliseconds(25), 1, std::chrono::seconds(2), configuration.hostname } + , networkQrCode{ configuration.password } + { + displayView.SetViewRegion(display.DisplayRegion()); + + layoutViewHorizontal.Add(networkQrCode); + layoutViewHorizontal.AddFill(layoutView, 1); + layoutView.AddFill(viewStatus.Scrolling(), 1); + layoutView.AddFill(viewNetworkStatus.Scrolling(), 1); + layoutViewHorizontal.ResetLayout(); + } +} diff --git a/postmaster/instantiations_st/Display.hpp b/postmaster/instantiations_st/Display.hpp new file mode 100644 index 0000000..da4c18f --- /dev/null +++ b/postmaster/instantiations_st/Display.hpp @@ -0,0 +1,55 @@ +#ifndef POSTMASTER_DISPLAY_HPP +#define POSTMASTER_DISPLAY_HPP + +#include "generated/echo/PostmasterConfiguration.pb.hpp" +#include "hal_st/stm32fxxx/I2cStm.hpp" +#include "postmaster/instantiations/ViewNetworkStatus.hpp" +#include "postmaster/instantiations/ViewStatus.hpp" +#include "preview/interfaces/BitmapPainter.hpp" +#include "preview/interfaces/BufferedDisplaySsd1306.hpp" +#include "preview/interfaces/ViewPainterBufferedDisplay.hpp" +#include "preview/interfaces/ViewRepainter.hpp" +#include "preview/views/HorizontalLayout.hpp" +#include "preview/views/VerticalLayout.hpp" +#include "preview/views/ViewPanel.hpp" +#include "preview/views/ViewRotating.hpp" +#include "preview/views/ViewScrolling.hpp" + +namespace main_ +{ + struct Display + { + Display(const postmaster_configuration::Configuration& configuration); + + hal::GpioPinStm scl{ hal::Port::B, 8, hal::Drive::OpenDrain }; + hal::GpioPinStm sda{ hal::Port::B, 9, hal::Drive::OpenDrain }; + + hal::I2cStm::Config config{ []() + { + hal::I2cStm::Config result; +#if defined(STM32F0) || defined(STM32F7) + result.timing = 0x10304d4d; +#endif + return result; + }() }; + hal::I2cStm i2c{ 1, scl, sda, config }; + + hal::BitmapPainterCanonical bitmapPainter; + services::BufferedDisplaySsd1306 display{ i2c }; + services::ViewPainterBufferedDisplay::WithBuffer<32, 128, infra::PixelFormat::blackandwhite> painter{ display, bitmapPainter }; + services::ViewPainterAlignedForSsd1306 painterAligned{ painter }; + + services::ViewRotating::WithView>> displayView{ infra::RightAngle::angle_270, infra::Colour::black }; + + services::ViewRepainterPaintWhenDirty repainter{ painterAligned, displayView }; + + services::HorizontalLayout& layoutViewHorizontal{ displayView.SubView().SubView() }; + services::VerticalLayout::WithMaxViews<2> layoutView; + + services::ViewScrolling::WithView viewStatus; + services::ViewScrolling::WithView viewNetworkStatus{ std::chrono::milliseconds(25), 1, std::chrono::seconds(2) }; + application::ViewNetworkQrCode networkQrCode; + }; +} + +#endif diff --git a/postmaster/instantiations_st/EthernetPostmaster.hpp b/postmaster/instantiations_st/EthernetPostmaster.hpp new file mode 100644 index 0000000..735b79e --- /dev/null +++ b/postmaster/instantiations_st/EthernetPostmaster.hpp @@ -0,0 +1,49 @@ +#ifndef POSTMASTER_ETHERNET_POSTMASTER_HPP +#define POSTMASTER_ETHERNET_POSTMASTER_HPP + +#include "hal_st/stm32fxxx/ResetStm.hpp" +#include "hal_st/synchronous_stm32fxxx/SynchronousRandomDataGeneratorStm.hpp" +#include "hal_st_lwip/instantiations_lwip/Ethernet.hpp" +#include "postmaster/instantiations/Configuration.hpp" +#include "postmaster/instantiations/ViewStatus.hpp" +#include "postmaster/instantiations_st/NetworkConnected.hpp" + +namespace main_ +{ + struct EthernetPostmaster + { + EthernetPostmaster(infra::MemoryRange> ethernetPins, Configuration& configuration, hal::Flash& upgradePack, UartCreator& uartProgrammerCreator, UartCreator& uartExternalCreator, application::ViewStatus& viewStatus) + : upgradePack(upgradePack) + , uartProgrammerCreator(uartProgrammerCreator) + , uartExternalCreator(uartExternalCreator) + , viewStatus(viewStatus) + , hostname{ configuration.store, configuration.store.Configuration().hostname } + , attributes{ configuration.store, configuration.store.Configuration().attributes } + , password{ configuration.store, configuration.store.Configuration().password } + , authentication{ password, randomDataGenerator } + , networkCreator([this](infra::Optional& value, services::LightweightIp& lightweightIp) + { + value.Emplace(lightweightIp, hostname, attributes, password, authentication, this->upgradePack, this->uartProgrammerCreator, this->uartExternalCreator, this->reset, this->viewStatus); + }) + , ethernet{ ethernetPins, configuration.DeviceMacAddress(), configuration.store.Configuration().hostname, randomDataGenerator, networkCreator } + {} + + hal::Flash& upgradePack; + UartCreator& uartProgrammerCreator; + UartCreator& uartExternalCreator; + application::ViewStatus& viewStatus; + + hal::ResetStm reset; + hal::SynchronousRandomDataGeneratorStm randomDataGenerator; + + services::ConfigurationStoreAccess hostname; + services::ConfigurationStoreAccess attributes; + services::ConfigurationStoreAccess password; + application::Authentication authentication; + infra::Creator networkCreator; + + main_::Ethernet<3, 0, 4> ethernet; + }; +} + +#endif diff --git a/postmaster/instantiations_st/Flash.cpp b/postmaster/instantiations_st/Flash.cpp new file mode 100644 index 0000000..fb8c0c1 --- /dev/null +++ b/postmaster/instantiations_st/Flash.cpp @@ -0,0 +1,10 @@ +#include "postmaster/instantiations_st/Flash.hpp" + +namespace main_ +{ + Flash::Flash(hal::DmaStm& dma) + : transmitStream(dma, hal::DmaChannelId{ 1, 5, 0 }) + , receiveStream(dma, hal::DmaChannelId{ 1, 0, 0 }) + , spiMaster(transmitStream, receiveStream, 3, spiClock, spiMiso, spiMosi) + {} +} diff --git a/postmaster/instantiations_st/Flash.hpp b/postmaster/instantiations_st/Flash.hpp new file mode 100644 index 0000000..df0a1c3 --- /dev/null +++ b/postmaster/instantiations_st/Flash.hpp @@ -0,0 +1,51 @@ +#ifndef POSTMASTER_FLASH_HPP +#define POSTMASTER_FLASH_HPP + +#include "hal_st/stm32fxxx/FlashInternalStm.hpp" +#include "hal_st/stm32fxxx/GpioStm.hpp" +#include "hal_st/stm32fxxx/SpiMasterStmDma.hpp" +#include "services/util/FlashRegion.hpp" +#include "services/util/FlashSpi.hpp" +#include "services/util/SpiMasterWithChipSelect.hpp" + +// #define UPG_IN_INTERNAL_FLASH + +#ifdef UPG_IN_INTERNAL_FLASH +extern uint8_t _flash_start; +extern uint8_t _flash_end; +#endif + +namespace main_ +{ + struct Flash + { + Flash(hal::DmaStm& dma); + + hal::GpioPinStm spiChipSelect{ hal::Port::A, 15 }; + hal::GpioPinStm spiClock{ hal::Port::C, 10 }; + hal::GpioPinStm spiMiso{ hal::Port::C, 11 }; + hal::GpioPinStm spiMosi{ hal::Port::C, 12 }; + hal::DmaStm::TransmitStream transmitStream; + hal::DmaStm::ReceiveStream receiveStream; + hal::SpiMasterStmDma spiMaster; + services::SpiMasterWithChipSelect spiMasterWithChipSelect{ spiMaster, spiChipSelect }; + services::FlashSpi flash{ spiMasterWithChipSelect }; + + std::array configurationStore{ services::FlashRegion{ flash, 0, 1 }, services::FlashRegion{ flash, 1, 1 } }; +#ifdef UPG_IN_INTERNAL_FLASH + std::array sectorSizes{ + 0x08000, 0x08000, 0x08000, 0x08000, + 0x20000, 0x40000, 0x40000, 0x40000, + 0x40000, 0x40000, 0x40000, 0x40000 + }; + + infra::ConstByteRange flashMemory{ &_flash_start, &_flash_end }; + hal::FlashInternalStm internalFlash{ infra::MakeRange(sectorSizes), flashMemory }; + services::FlashRegion upgradePack{ internalFlash, 10, 2 }; +#else + services::FlashRegion upgradePack{ flash, 2, 510 }; +#endif + }; +} + +#endif diff --git a/postmaster/instantiations_st/HttpServerSt.cpp b/postmaster/instantiations_st/HttpServerSt.cpp new file mode 100644 index 0000000..843d930 --- /dev/null +++ b/postmaster/instantiations_st/HttpServerSt.cpp @@ -0,0 +1,11 @@ +#include "postmaster/instantiations_st/HttpServerSt.hpp" + +namespace main_ +{ + HttpServerSt::HttpServerSt(services::ConnectionFactory& connectionFactory, services::ConfigurationStoreAccess& hostname, + services::ConfigurationStoreAccess& attributes, services::ConfigurationStoreAccess& password, + application::Authentication& authentication, application::PostmasterDiscovery& postmasterDiscovery, + UartCreator& uartProgrammerCreator, UartCreator& uartExternalCreator, hal::Flash& upgradeFlash, hal::Reset& reset, const infra::Function& reporter, const infra::Function& receivingTarget, const infra::Function& receivingSelf) + : HttpServer(connectionFactory, hostname, attributes, password, authentication, postmasterDiscovery, uartProgrammerCreator, uartExternalCreator, upgradeFlash, reset, reporter, receivingTarget, receivingSelf, resetTarget, boot0) + {} +} diff --git a/postmaster/instantiations_st/HttpServerSt.hpp b/postmaster/instantiations_st/HttpServerSt.hpp new file mode 100644 index 0000000..6c4fc91 --- /dev/null +++ b/postmaster/instantiations_st/HttpServerSt.hpp @@ -0,0 +1,112 @@ +#ifndef POSTMASTER_HTTP_SERVER_ST_HPP +#define POSTMASTER_HTTP_SERVER_ST_HPP + +#include "hal/interfaces/SerialCommunication.hpp" +#include "hal_st/stm32fxxx/GpioStm.hpp" +#include "hal_st/stm32fxxx/UartStm.hpp" +#include "infra/util/SharedPtr.hpp" +#include "postmaster/instantiations/HttpServer.hpp" +#include "services/network/Connection.hpp" + +namespace main_ +{ + struct HttpServerSt + : public HttpServer + { + HttpServerSt(services::ConnectionFactory& connectionFactory, services::ConfigurationStoreAccess& hostname, + services::ConfigurationStoreAccess& attributes, services::ConfigurationStoreAccess& password, + application::Authentication& authentication, application::PostmasterDiscovery& postmasterDiscovery, + UartCreator& uartProgrammerCreator, UartCreator& uartExternalCreator, hal::Flash& upgradeFlash, hal::Reset& reset, const infra::Function& reporter, const infra::Function& receivingTarget, const infra::Function& receivingSelf); + + hal::GpioPinStm resetTarget{ hal::Port::B, 4 }; + hal::GpioPinStm boot0{ hal::Port::B, 5 }; + }; + + class TraceForwarder + : private services::ServerConnectionObserverFactory + , private services::ConnectionObserver + , private hal::BufferedSerialCommunicationObserver + { + public: + TraceForwarder(services::ConnectionFactory& connectionFactory, hal::BufferedSerialCommunication& traceUart) + : hal::BufferedSerialCommunicationObserver(traceUart) + , listener(connectionFactory.Listen(1023, *this)) + {} + + private: + // Implementation of ServerConnectionObserverFactory + void ConnectionAccepted(infra::AutoResetFunction connectionObserver)>&& createdObserver, services::IPAddress address) override + { + waitingConnection = std::move(createdObserver); + + if (services::ConnectionObserver::IsAttached()) + services::ConnectionObserver::Close(); + + TryAcceptConnection(); + } + + private: + // Implementation of ConnectionObserver + void SendStreamAvailable(infra::SharedPtr&& writer) override + { + requestingStream = false; + + infra::StreamErrorPolicy errorPolicy; + auto& reader = hal::BufferedSerialCommunicationObserver::Subject().Reader(); + + while (!reader.Empty() && !writer->Empty()) + { + auto range = reader.ExtractContiguousRange(writer->Available()); + writer->Insert(range, errorPolicy); + } + + hal::BufferedSerialCommunicationObserver::Subject().AckReceived(); + + DataReceived(); // Trigger a new RequestSendStream if necessary + } + + // Implementation of both ConnectionObserver and BufferedSerialCommunicationObserver; only the latter data is processed + void DataReceived() override + { + if (!requestingStream && !hal::BufferedSerialCommunicationObserver::Subject().Reader().Empty() && services::ConnectionObserver::IsAttached()) + { + requestingStream = true; + services::ConnectionObserver::Subject().RequestSendStream(services::ConnectionObserver::Subject().MaxSendStreamSize()); + } + } + + private: + void TryAcceptConnection() + { + requestingStream = false; + if (waitingConnection != nullptr && !access.Referenced()) + waitingConnection(access.MakeShared(static_cast(*this))); + } + + private: + infra::SharedPtr listener; + + infra::AccessedBySharedPtr access{[this]() + { + TryAcceptConnection(); + }}; + infra::AutoResetFunction connectionObserver)> waitingConnection; + + bool requestingStream = false; + }; + + struct TraceForwarderSt + { + TraceForwarderSt(services::ConnectionFactory& connectionFactory) + : forwarder(connectionFactory, bufferedTraceUart) + {} + + hal::GpioPinStm traceRx{ hal::Port::C, 7 }; + hal::UartStm traceUart{ 6, hal::dummyPinStm, traceRx }; + hal::BufferedSerialCommunicationOnUnbuffered::WithStorage<1024> bufferedTraceUart{ traceUart }; + + TraceForwarder forwarder; + }; +} + +#endif diff --git a/postmaster/instantiations_st/NetworkConnected.hpp b/postmaster/instantiations_st/NetworkConnected.hpp new file mode 100644 index 0000000..6213505 --- /dev/null +++ b/postmaster/instantiations_st/NetworkConnected.hpp @@ -0,0 +1,54 @@ +#ifndef POSTMASTER_NETWORK_CONNECTED_HPP +#define POSTMASTER_NETWORK_CONNECTED_HPP + +#include "generated/Version.h" +#include "lwip/lwip_cpp/LightweightIp.hpp" +#include "postmaster/instantiations/EchoServer.hpp" +#include "postmaster/instantiations/Mdns.hpp" +#include "postmaster/instantiations/MdnsDiscovery.hpp" +#include "postmaster/instantiations/ViewNetworkStatus.hpp" +#include "postmaster/instantiations/ViewStatus.hpp" +#include "postmaster/instantiations_st/HttpServerSt.hpp" + +namespace main_ +{ + struct NetworkConnected + : public services::Stoppable + { + NetworkConnected(services::LightweightIp& lightweightIp, services::ConfigurationStoreAccess& hostname, + services::ConfigurationStoreAccess& attributes, services::ConfigurationStoreAccess& password, + application::Authentication& authentication, + hal::Flash& upgradePack, UartCreator& uartProgrammerCreator, UartCreator& uartExternalCreator, hal::Reset& reset, application::ViewStatus& viewStatus) + : mdns{ *hostname, lightweightIp, lightweightIp, lightweightIp.GetIPv4Address(), Postmaster::generated::VERSION, Postmaster::generated::VERSION_FULL, *attributes } + , mdnsDiscovery{ lightweightIp, lightweightIp } + , httpServer{ lightweightIp, hostname, attributes, password, authentication, mdnsDiscovery.discovery, uartProgrammerCreator, uartExternalCreator, upgradePack, reset, [&viewStatus](bool open, services::IPAddress address) + { + viewStatus.SetConnectionOpen(open, address); + }, + [&viewStatus](bool receiving) + { + viewStatus.SetReceivingTarget(receiving); + }, + [&viewStatus](bool receiving) + { + viewStatus.SetReceivingSelf(receiving); + } } + , echoServer{ lightweightIp, uartExternalCreator, 1235 } + , traceForwarder(lightweightIp) + {} + + void Stop(const infra::Function& onDone) override + { + httpServer.Stop(onDone); + } + + main_::Mdns mdns; + main_::MdnsDiscovery mdnsDiscovery; + main_::HttpServerSt httpServer; + main_::EchoServer echoServer; + main_::SingleConnectionLink link{ httpServer.server, echoServer.listener }; + main_::TraceForwarderSt traceForwarder; + }; +} + +#endif diff --git a/postmaster/postmaster_stm32f407/CMakeLists.txt b/postmaster/postmaster_stm32f407/CMakeLists.txt new file mode 100644 index 0000000..3fe5ab5 --- /dev/null +++ b/postmaster/postmaster_stm32f407/CMakeLists.txt @@ -0,0 +1,31 @@ +add_executable(postmaster.postmaster_stm32f407) +emil_build_for(postmaster.postmaster_stm32f407 TARGET_MCU stm32f407 PREREQUISITE_CONFIG Release RelWithDebInfo MinSizeRel) + +target_include_directories(postmaster.postmaster_stm32f407 PUBLIC + "$" + "$" +) + +target_sources(postmaster.postmaster_stm32f407 PRIVATE + Main.cpp +) + +add_version_header_dependency(postmaster.postmaster_stm32f407 postmaster.version) + +target_link_libraries(postmaster.postmaster_stm32f407 PUBLIC + services.tracer + services.util + hal_st.instantiations + hal_st.instantiations_lwip + hal_st.synchronous_stm32fxxx + postmaster.instantiations_st +) + +halst_target_linker_scripts(TARGET postmaster.postmaster_stm32f407 LINKER_SCRIPTS + ${CMAKE_CURRENT_LIST_DIR}/../bootloader/mem_stm32f407.ld + ${CMAKE_CURRENT_LIST_DIR}/sections.ld +) + +halst_target_default_init(postmaster.postmaster_stm32f407) + +emil_generate_artifacts(TARGET postmaster.postmaster_stm32f407 BIN HEX MAP) diff --git a/postmaster/postmaster_stm32f407/Main.cpp b/postmaster/postmaster_stm32f407/Main.cpp new file mode 100644 index 0000000..7be1337 --- /dev/null +++ b/postmaster/postmaster_stm32f407/Main.cpp @@ -0,0 +1,113 @@ +#include "hal_st/instantiations/NucleoTracerInfrastructure.hpp" +#include "hal_st/instantiations/StmEventInfrastructure.hpp" +#include "hal_st/stm32fxxx/DefaultClockDiscoveryF407G.hpp" +#include "hal_st/stm32fxxx/UniqueDeviceId.hpp" +#include "hal_st/stm32fxxx/WatchDogStm.hpp" +#include "postmaster/instantiations/Configuration.hpp" +#include "postmaster/instantiations_st/ConvertUart.hpp" +#include "postmaster/instantiations_st/Display.hpp" +#include "postmaster/instantiations_st/EthernetPostmaster.hpp" +#include "postmaster/instantiations_st/Flash.hpp" +#include "services/tracer/GlobalTracer.hpp" + +const uint32_t* interruptStack = nullptr; + +extern "C" [[gnu::used]] void DefaultHandlerImpl(const uint32_t* stack) +{ + interruptStack = stack; + hal::InterruptTable::Instance().Invoke(hal::ActiveInterrupt()); + interruptStack = nullptr; +} + +extern "C" [[gnu::naked]] void Default_Handler_Forwarded() +{ + asm volatile( + "tst lr, #4 \n" + "ite eq \n" + "mrseq r0, msp \n" + "mrsne r0, psp \n" + "b DefaultHandlerImpl \n"); +} + +void HardFault() +{ + auto pc = interruptStack[6]; + services::GlobalTracer().Trace() << "Hard fault at 0x" << infra::hex << infra::Width(8, '0') << pc; + std::abort(); +} + +void WatchdogTimeout() +{ + auto pc = interruptStack[6]; + services::GlobalTracer().Trace() << "Watchdog interrupt at 0x" << infra::hex << infra::Width(8, '0') << pc; + std::abort(); +} + +unsigned int hse_value = 8000000; +extern uint8_t _application_start; + +static constexpr std::array, 9> ethernetPins = { { { hal::Port::A, 2 }, + { hal::Port::C, 1 }, + { hal::Port::A, 1 }, + { hal::Port::A, 7 }, + { hal::Port::C, 4 }, + { hal::Port::C, 5 }, + { hal::Port::B, 11 }, + { hal::Port::B, 12 }, + { hal::Port::B, 13 } } }; + +namespace main_ +{ + struct TargetUarts + { + TargetUarts(hal::DmaStm& dma) + : uartTransmitStream{ dma, hal::DmaChannelId{ 2, 7, 4 } } + , uartProgrammerCreator{ [this, &dma](infra::Optional& value, const UartConfig& config) + { + value.Emplace(uartTransmitStream, 1, uartProgrammerTx, uartProgrammerRx, Convert(config)); + } } + , uartExternalCreator{ [this, &dma](infra::Optional& value, const UartConfig& config) + { + value.Emplace(uartTransmitStream, 1, uartExternalTx, uartExternalRx, Convert(config)); + } } + {} + + hal::DmaStm::TransmitStream uartTransmitStream; + hal::GpioPinStm uartProgrammerTx{ hal::Port::B, 6 }; + hal::GpioPinStm uartProgrammerRx{ hal::Port::A, 10 }; + infra::Creator uartProgrammerCreator; + + hal::GpioPinStm uartExternalTx{ hal::Port::A, 9 }; + hal::GpioPinStm uartExternalRx{ hal::Port::B, 7 }; + infra::Creator uartExternalCreator; + }; +} + +int main() +{ + SCB->VTOR = reinterpret_cast(&_application_start); + HAL_Init(); + ConfigureDefaultClockDiscoveryF407G(); + + static main_::StmEventInfrastructure eventInfrastructure; + static hal::ImmediateInterruptHandler hardfaultRegistration(/*HardFault_IRQn*/ static_cast(-13), HardFault); + static hal::WatchDogStm watchdog(WatchdogTimeout); + static main_::NucleoF767ziTracerInfrastructure tracer; + services::SetGlobalTracerInstance(tracer.tracer); + + tracer.tracer.Trace() << "Version: " << Postmaster::generated::VERSION_FULL; + + static hal::DmaStm dma; + static main_::Flash flash(dma); + static main_::Configuration configuration( + flash.configurationStore[0], flash.configurationStore[1], hal::UniqueDeviceId(), []() + { + static main_::TargetUarts targetUarts(dma); + static main_::Display display(configuration.store.Configuration()); + static main_::EthernetPostmaster ethernet(ethernetPins, configuration, flash.upgradePack, targetUarts.uartProgrammerCreator, targetUarts.uartExternalCreator, display.viewStatus.SubView()); + }, + tracer.tracer); + + eventInfrastructure.Run(); + __builtin_unreachable(); +} diff --git a/postmaster/postmaster_stm32f407/sections.ld b/postmaster/postmaster_stm32f407/sections.ld new file mode 100644 index 0000000..b3d98c7 --- /dev/null +++ b/postmaster/postmaster_stm32f407/sections.ld @@ -0,0 +1,170 @@ +__stack = ORIGIN(RAM) + LENGTH(RAM); +_estack = __stack; /* STM specific definition */ + +__Main_Stack_Size = 1024 ; +PROVIDE(_Main_Stack_Size = __Main_Stack_Size); + +__Main_Stack_Limit = __stack - __Main_Stack_Size ; +PROVIDE (_Main_Stack_Limit = __Main_Stack_Limit); + +PROVIDE(_Heap_Begin = __end__); +PROVIDE(_Heap_Limit = __stack - __Main_Stack_Size); + +EXTERN(__isr_vectors) + +ENTRY(Reset_Handler) + +SECTIONS +{ + .isr_vector : ALIGN(4) + { + FILL(0xFF) + + KEEP(*(.isr_vector)) + *(.after_vectors .after_vectors.*) + } >APPLICATION + + .inits : ALIGN(4) + { + /* + * These are the old initialisation sections, intended to contain + * naked code, with the prologue/epilogue added by crti.o/crtn.o + * when linking with startup files. The standalone startup code + * currently does not run these, better use the init arrays below. + */ + KEEP(*(.init)) + KEEP(*(.fini)) + _fini = .; + + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP(*(.preinit_array_sysinit .preinit_array_sysinit.*)) + KEEP(*(.preinit_array_platform .preinit_array_platform.*)) + KEEP(*(.preinit_array .preinit_array.*)) + PROVIDE_HIDDEN (__preinit_array_end = .); + + . = ALIGN(4); + PROVIDE_HIDDEN (__init_array_start = .); + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array)) + PROVIDE_HIDDEN (__init_array_end = .); + + . = ALIGN(4); + PROVIDE_HIDDEN (__fini_array_start = .); + KEEP(*(SORT(.fini_array.*))) + KEEP(*(.fini_array)) + PROVIDE_HIDDEN (__fini_array_end = .); + } >APPLICATION + + .text : ALIGN(4) + { + *(.text .text.*) + + KEEP(*(.eh_frame*)) + + *(.glue_7) /* Linker-generated stubs for ARM code calling Thumb code */ + *(.glue_7t) /* Linker-generated stubs for Thumb code calling ARM code */ + + . = ALIGN(4); + _etext = .; + } >APPLICATION + + .rodata : ALIGN(4) + { + *(.rodata) + *(.rodata*) + } >APPLICATION + + /* Exception sections */ + .ARM.extab : ALIGN(4) + { + *(.ARM.extab* .gnu.linkonce.armextab.*) + } > APPLICATION + + .ARM.exidx : ALIGN(4) + { + __exidx_start = .; + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + . = ALIGN(4); + __exidx_end = .; + } > APPLICATION + + _sidata = LOADADDR(.data); + + .data : ALIGN(4) + { + FILL(0xFF) + _sdata = .; + __data_start__ = _sdata; + *(.data .data.*) + . = ALIGN(4); + _edata = .; + __data_end__ = _edata; + + } >RAM AT>APPLICATION + + .bss (NOLOAD) : ALIGN(4) + { + _sbss = .; + __bss_start__ = _sbss; + *(.bss .bss.*) + *(COMMON) + . = ALIGN(4); + _ebss = .; + __bss_end__ = _ebss; + } >RAM + + .noinit (NOLOAD) : ALIGN(4) + { + _noinit = .; + *(.noinit .noinit.*) + . = ALIGN(4) ; + _end_noinit = .; + } >RAM + + PROVIDE(__end__ = _end_noinit); + + MAPPING_TABLE (NOLOAD) : { *(MAPPING_TABLE) } >RAM_SHARED + MB_MEM1 (NOLOAD) : { *(MB_MEM1) } >RAM_SHARED + MB_MEM2 (NOLOAD) : { _sMB_MEM2 = . ; *(MB_MEM2) ; _eMB_MEM2 = . ; } >RAM_SHARED + + ._check_stack : ALIGN(4) + { + . = . + __Main_Stack_Size ; + } >RAM + + /* Stabs debugging sections. */ + .stab 0 : { *(.stab) } + .stabstr 0 : { *(.stabstr) } + .stab.excl 0 : { *(.stab.excl) } + .stab.exclstr 0 : { *(.stab.exclstr) } + .stab.index 0 : { *(.stab.index) } + .stab.indexstr 0 : { *(.stab.indexstr) } + .comment 0 : { *(.comment) } + + /* DWARF debug sections. + * Symbols in the DWARF debugging sections are relative to the beginning + * of the section so we begin them at 0. + */ + /* DWARF 1 */ + .debug 0 : { *(.debug) } + .line 0 : { *(.line) } + /* GNU DWARF 1 extensions */ + .debug_srcinfo 0 : { *(.debug_srcinfo) } + .debug_sfnames 0 : { *(.debug_sfnames) } + /* DWARF 1.1 and DWARF 2 */ + .debug_aranges 0 : { *(.debug_aranges) } + .debug_pubnames 0 : { *(.debug_pubnames) } + /* DWARF 2 */ + .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_macinfo 0 : { *(.debug_macinfo) } + /* SGI/MIPS DWARF 2 extensions */ + .debug_weaknames 0 : { *(.debug_weaknames) } + .debug_funcnames 0 : { *(.debug_funcnames) } + .debug_typenames 0 : { *(.debug_typenames) } + .debug_varnames 0 : { *(.debug_varnames) } +} diff --git a/postmaster/programmer/CMakeLists.txt b/postmaster/programmer/CMakeLists.txt new file mode 100644 index 0000000..4ad2c92 --- /dev/null +++ b/postmaster/programmer/CMakeLists.txt @@ -0,0 +1,33 @@ +add_library(postmaster.programmer STATIC) + +target_include_directories(postmaster.programmer PUBLIC + "$" + "$" +) + +target_sources(postmaster.programmer PRIVATE + FirmwareReceptor.hpp + FirmwareReceptorReporting.cpp + FirmwareReceptorReporting.hpp + FirmwareReceptorResetTarget.cpp + FirmwareReceptorResetTarget.hpp + FirmwareReceptorResetWhenDone.cpp + FirmwareReceptorResetWhenDone.hpp + FirmwareReceptorToFlash.cpp + FirmwareReceptorToFlash.hpp + HttpPageFirmware.cpp + HttpPageFirmware.hpp + TargetUartConnection.cpp + TargetUartConnection.hpp +) + +target_link_libraries(postmaster.programmer PUBLIC + hal.interfaces + infra.timer + infra.util + services.tracer + services.util + services.network + services.st_util + protobuf.echo +) diff --git a/postmaster/programmer/FirmwareReceptor.hpp b/postmaster/programmer/FirmwareReceptor.hpp new file mode 100644 index 0000000..f6f668f --- /dev/null +++ b/postmaster/programmer/FirmwareReceptor.hpp @@ -0,0 +1,24 @@ +#ifndef POSTMASTER_PROGRAMMER_FIRMWARE_RECEPTOR_HPP +#define POSTMASTER_PROGRAMMER_FIRMWARE_RECEPTOR_HPP + +#include "infra/stream/InputStream.hpp" +#include "infra/util/SharedPtr.hpp" + +namespace application +{ + class FirmwareReceptor + { + protected: + FirmwareReceptor() = default; + FirmwareReceptor(const FirmwareReceptor& other) = delete; + FirmwareReceptor& operator=(const FirmwareReceptor& other) = delete; + virtual ~FirmwareReceptor() = default; + + public: + virtual void ReceptionStarted() = 0; + virtual void DataReceived(infra::SharedPtr&& reader) = 0; + virtual void ReceptionStopped() = 0; + }; +} + +#endif diff --git a/postmaster/programmer/FirmwareReceptorReporting.cpp b/postmaster/programmer/FirmwareReceptorReporting.cpp new file mode 100644 index 0000000..f5ba9a9 --- /dev/null +++ b/postmaster/programmer/FirmwareReceptorReporting.cpp @@ -0,0 +1,26 @@ +#include "postmaster/programmer/FirmwareReceptorReporting.hpp" + +namespace application +{ + FirmwareReceptorReporting::FirmwareReceptorReporting(FirmwareReceptor& delegate, const infra::Function& reporter) + : delegate(delegate) + , reporter(reporter) + {} + + void FirmwareReceptorReporting::ReceptionStarted() + { + reporter(true); + delegate.ReceptionStarted(); + } + + void FirmwareReceptorReporting::DataReceived(infra::SharedPtr&& reader) + { + delegate.DataReceived(std::move(reader)); + } + + void FirmwareReceptorReporting::ReceptionStopped() + { + delegate.ReceptionStopped(); + reporter(false); + } +} diff --git a/postmaster/programmer/FirmwareReceptorReporting.hpp b/postmaster/programmer/FirmwareReceptorReporting.hpp new file mode 100644 index 0000000..9137bca --- /dev/null +++ b/postmaster/programmer/FirmwareReceptorReporting.hpp @@ -0,0 +1,25 @@ +#ifndef POSTMASTER_PROGRAMMER_FIRMWARE_REPORTING_HPP +#define POSTMASTER_PROGRAMMER_FIRMWARE_REPORTING_HPP + +#include "infra/util/Function.hpp" +#include "postmaster/programmer/FirmwareReceptor.hpp" + +namespace application +{ + class FirmwareReceptorReporting + : public FirmwareReceptor + { + public: + FirmwareReceptorReporting(FirmwareReceptor& delegate, const infra::Function& reporter); + + virtual void ReceptionStarted() override; + virtual void DataReceived(infra::SharedPtr&& reader) override; + virtual void ReceptionStopped() override; + + private: + FirmwareReceptor& delegate; + infra::Function reporter; + }; +} + +#endif diff --git a/postmaster/programmer/FirmwareReceptorResetTarget.cpp b/postmaster/programmer/FirmwareReceptorResetTarget.cpp new file mode 100644 index 0000000..7804b48 --- /dev/null +++ b/postmaster/programmer/FirmwareReceptorResetTarget.cpp @@ -0,0 +1,39 @@ +#include "postmaster/programmer/FirmwareReceptorResetTarget.hpp" + +namespace application +{ + FirmwareReceptorResetTarget::FirmwareReceptorResetTarget(hal::GpioPin& reset, hal::GpioPin& boot0, infra::CreatorBase& delegateCreator) + : reset(reset) + , boot0(boot0) + , delegateCreator(delegateCreator) + {} + + void FirmwareReceptorResetTarget::ReceptionStarted() + { + { + hal::OutputPin activateReset(reset, false); + activateBoot0.Emplace(boot0, true); + + delegate.Emplace(delegateCreator); + } + + (*delegate)->ReceptionStarted(); + } + + void FirmwareReceptorResetTarget::DataReceived(infra::SharedPtr&& reader) + { + (*delegate)->DataReceived(std::move(reader)); + } + + void FirmwareReceptorResetTarget::ReceptionStopped() + { + (*delegate)->ReceptionStopped(); + + { + hal::OutputPin activateReset(reset, false); + activateBoot0 = infra::none; + + delegate = infra::none; + } + } +} diff --git a/postmaster/programmer/FirmwareReceptorResetTarget.hpp b/postmaster/programmer/FirmwareReceptorResetTarget.hpp new file mode 100644 index 0000000..3eaeea9 --- /dev/null +++ b/postmaster/programmer/FirmwareReceptorResetTarget.hpp @@ -0,0 +1,27 @@ +#include "hal/interfaces/Gpio.hpp" +#include "infra/util/Optional.hpp" +#include "infra/util/ProxyCreator.hpp" +#include "postmaster/programmer/FirmwareReceptor.hpp" + +namespace application +{ + class FirmwareReceptorResetTarget + : public FirmwareReceptor + { + public: + FirmwareReceptorResetTarget(hal::GpioPin& reset, hal::GpioPin& boot0, infra::CreatorBase& delegateCreator); + + virtual void ReceptionStarted() override; + virtual void DataReceived(infra::SharedPtr&& reader) override; + virtual void ReceptionStopped() override; + + private: + hal::GpioPin& reset; + hal::GpioPin& boot0; + + infra::CreatorBase& delegateCreator; + infra::Optional> delegate; + + infra::Optional activateBoot0; + }; +} diff --git a/postmaster/programmer/FirmwareReceptorResetWhenDone.cpp b/postmaster/programmer/FirmwareReceptorResetWhenDone.cpp new file mode 100644 index 0000000..166d674 --- /dev/null +++ b/postmaster/programmer/FirmwareReceptorResetWhenDone.cpp @@ -0,0 +1,29 @@ +#include "postmaster/programmer/FirmwareReceptorResetWhenDone.hpp" +#include "infra/util/PostAssign.hpp" + +namespace application +{ + FirmwareReceptorResetWhenDone::FirmwareReceptorResetWhenDone(FirmwareReceptor& delegate, hal::Reset& reset) + : delegate(delegate) + , reset(reset) + {} + + void FirmwareReceptorResetWhenDone::ReceptionStarted() + { + delegate.ReceptionStarted(); + } + + void FirmwareReceptorResetWhenDone::DataReceived(infra::SharedPtr&& reader) + { + delegate.DataReceived(std::move(reader)); + } + + void FirmwareReceptorResetWhenDone::ReceptionStopped() + { + delegate.ReceptionStopped(); + delayedReboot.Start(std::chrono::seconds(1), [this]() + { + reset.ResetModule("Firmare reception done"); + }); + } +} diff --git a/postmaster/programmer/FirmwareReceptorResetWhenDone.hpp b/postmaster/programmer/FirmwareReceptorResetWhenDone.hpp new file mode 100644 index 0000000..3c898ad --- /dev/null +++ b/postmaster/programmer/FirmwareReceptorResetWhenDone.hpp @@ -0,0 +1,27 @@ +#ifndef POSTMASTER_PROGRAMMER_FIRMWARE_RECEPTOR_RESET_WHEN_DONE_HPP +#define POSTMASTER_PROGRAMMER_FIRMWARE_RECEPTOR_RESET_WHEN_DONE_HPP + +#include "hal/interfaces/Reset.hpp" +#include "infra/timer/Timer.hpp" +#include "postmaster/programmer/FirmwareReceptor.hpp" + +namespace application +{ + class FirmwareReceptorResetWhenDone + : public FirmwareReceptor + { + public: + FirmwareReceptorResetWhenDone(FirmwareReceptor& delegate, hal::Reset& reset); + + virtual void ReceptionStarted() override; + virtual void DataReceived(infra::SharedPtr&& reader) override; + virtual void ReceptionStopped() override; + + private: + FirmwareReceptor& delegate; + hal::Reset& reset; + infra::TimerSingleShot delayedReboot; + }; +} + +#endif diff --git a/postmaster/programmer/FirmwareReceptorToFlash.cpp b/postmaster/programmer/FirmwareReceptorToFlash.cpp new file mode 100644 index 0000000..475fef3 --- /dev/null +++ b/postmaster/programmer/FirmwareReceptorToFlash.cpp @@ -0,0 +1,47 @@ +#include "postmaster/programmer/FirmwareReceptorToFlash.hpp" +#include "infra/util/PostAssign.hpp" + +namespace application +{ + FirmwareReceptorToFlash::FirmwareReceptorToFlash(hal::Flash& flash) + : flash(flash) + {} + + void FirmwareReceptorToFlash::ReceptionStarted() + { + index = 0; + + busy = true; + flash.EraseAll([this]() + { + OnEraseDone(); + }); + } + + void FirmwareReceptorToFlash::DataReceived(infra::SharedPtr&& reader) + { + this->reader = std::move(reader); + TryWrite(); + } + + void FirmwareReceptorToFlash::ReceptionStopped() + {} + + void FirmwareReceptorToFlash::OnEraseDone() + { + busy = false; + TryWrite(); + } + + void FirmwareReceptorToFlash::TryWrite() + { + if (!busy && reader != nullptr) + { + auto range = reader->ExtractContiguousRange(1024 * 1024); + flash.WriteBuffer(range, infra::PostAssign(index, index + range.size()), [this]() + { + reader = nullptr; + }); + } + } +} diff --git a/postmaster/programmer/FirmwareReceptorToFlash.hpp b/postmaster/programmer/FirmwareReceptorToFlash.hpp new file mode 100644 index 0000000..b2e43ff --- /dev/null +++ b/postmaster/programmer/FirmwareReceptorToFlash.hpp @@ -0,0 +1,31 @@ +#ifndef POSTMASTER_PROGRAMMER_FIRMWARE_RECEPTOR_TO_FLASH_HPP +#define POSTMASTER_PROGRAMMER_FIRMWARE_RECEPTOR_TO_FLASH_HPP + +#include "hal/interfaces/Flash.hpp" +#include "postmaster/programmer/FirmwareReceptor.hpp" + +namespace application +{ + class FirmwareReceptorToFlash + : public FirmwareReceptor + { + public: + FirmwareReceptorToFlash(hal::Flash& flash); + + virtual void ReceptionStarted() override; + virtual void DataReceived(infra::SharedPtr&& reader) override; + virtual void ReceptionStopped() override; + + private: + void OnEraseDone(); + void TryWrite(); + + private: + hal::Flash& flash; + infra::SharedPtr reader; + uint32_t index = 0; + bool busy = false; + }; +} + +#endif diff --git a/postmaster/programmer/HttpPageFirmware.cpp b/postmaster/programmer/HttpPageFirmware.cpp new file mode 100644 index 0000000..1a2b798 --- /dev/null +++ b/postmaster/programmer/HttpPageFirmware.cpp @@ -0,0 +1,85 @@ +#include "postmaster/programmer/HttpPageFirmware.hpp" +#include "services/network/HttpErrors.hpp" + +namespace application +{ + HttpPageFirmware::HttpPageFirmware(infra::BoundedConstString path) + : path(path) + {} + + void HttpPageFirmware::SetReceptor(FirmwareReceptor& newReceptor) + { + receptor = &newReceptor; + } + + void HttpPageFirmware::ResetReceptor(infra::BoundedConstString reason) + { + // ResetReceptor may only be called when the network has gone down and the connections have already been destructed. + // When all connections have been cleaned up, then this object is not active anymore + really_assert(!active); + + infra::ReConstruct(response, reason); + receptor = nullptr; + } + + bool HttpPageFirmware::ServesRequest(const infra::Tokenizer& pathTokens) const + { + return pathTokens.TokenAndRest(0) == path; + } + + void HttpPageFirmware::RequestReceived(services::HttpRequestParser& parser, services::HttpServerConnection& connection) + { + if (parser.Verb() != services::HttpVerb::post) + connection.SendResponse(services::HttpResponseBadRequest::Instance()); + else + { + if (receptor != nullptr) + { + this->connection = &connection; + receptor->ReceptionStarted(); + active = true; + } + else + connection.SendResponse(response); + } + } + + void HttpPageFirmware::DataReceived(infra::SharedPtr&& reader) + { + if (receptor != nullptr) + { + this->reader = std::move(reader); + receptor->DataReceived(access.MakeShared(*this->reader)); + } + } + + void HttpPageFirmware::Close() + { + active = false; + + if (receptor != nullptr && !access.Referenced()) + { + receptor->ReceptionStopped(); + connection->SendResponse(services::httpResponseNoContent); + } + + connection = nullptr; + } + + void HttpPageFirmware::ReaderUnreferenced() + { + if (!active) + { + receptor->ReceptionStopped(); + + if (connection != nullptr) + connection->SendResponse(services::httpResponseNoContent); + } + + reader = nullptr; + } + + HttpPageFirmware::HttpResponseNoReceptor::HttpResponseNoReceptor(infra::BoundedConstString reason) + : services::SimpleHttpResponse("500 Internal Server Error", reason) + {} +} diff --git a/postmaster/programmer/HttpPageFirmware.hpp b/postmaster/programmer/HttpPageFirmware.hpp new file mode 100644 index 0000000..709aa25 --- /dev/null +++ b/postmaster/programmer/HttpPageFirmware.hpp @@ -0,0 +1,51 @@ +#ifndef POSTMASTER_PROGRAMMER_HTTP_PAGE_FIRMWARE_HPP +#define POSTMASTER_PROGRAMMER_HTTP_PAGE_FIRMWARE_HPP + +#include "postmaster/programmer/FirmwareReceptor.hpp" +#include "services/network/HttpServer.hpp" + +namespace application +{ + class HttpPageFirmware + : public services::HttpPage + { + public: + HttpPageFirmware(infra::BoundedConstString path); + + void SetReceptor(FirmwareReceptor& newReceptor); + void ResetReceptor(infra::BoundedConstString reason); + + // Implementation of HttpPage + virtual bool ServesRequest(const infra::Tokenizer& pathTokens) const override; + virtual void RequestReceived(services::HttpRequestParser& parser, services::HttpServerConnection& connection) override; + virtual void DataReceived(infra::SharedPtr&& reader) override; + virtual void Close() override; + + private: + void ReaderUnreferenced(); + + private: + class HttpResponseNoReceptor + : public services::SimpleHttpResponse + { + public: + HttpResponseNoReceptor(infra::BoundedConstString reason); + }; + + private: + infra::BoundedConstString path; + FirmwareReceptor* receptor = nullptr; + HttpResponseNoReceptor response{ "Receptor not set" }; + + services::HttpServerConnection* connection = nullptr; + bool active = false; + + infra::SharedPtr reader; + infra::AccessedBySharedPtr access{ [this]() + { + ReaderUnreferenced(); + } }; + }; +} + +#endif diff --git a/postmaster/programmer/TargetUartConnection.cpp b/postmaster/programmer/TargetUartConnection.cpp new file mode 100644 index 0000000..e0dfc86 --- /dev/null +++ b/postmaster/programmer/TargetUartConnection.cpp @@ -0,0 +1,70 @@ +#include "postmaster/programmer/TargetUartConnection.hpp" + +namespace application +{ + TargetUartConnection::TargetUartConnection(infra::ByteRange buffer, hal::SerialCommunication& serial) + : queue{ buffer, [this]() + { + ReceivedSerialData(); + } } + , serial(serial) + { + serial.ReceiveData([this](infra::ConstByteRange data) + { + data.shrink_from_back_to(queue.EmptySize() - queue.Size()); + queue.AddFromInterrupt(data); + }); + } + + TargetUartConnection::~TargetUartConnection() + { + serial.ReceiveData(nullptr); + } + + void TargetUartConnection::SendStreamAvailable(infra::SharedPtr&& streamWriter) + { + streamRequested = false; + + infra::DataOutputStream::WithErrorPolicy stream(*streamWriter); + while (stream.Available() != 0 && !queue.Empty()) + { + auto data = queue.ContiguousRange(0); + queue.Consume(data.size()); + data.shrink_from_back_to(stream.Available()); + stream << data; + } + + streamWriter = nullptr; + ReceivedSerialData(); + } + + void TargetUartConnection::DataReceived() + { + if (receiveStream == nullptr && IsAttached()) + { + receiveStream = Subject().ReceiveStream(); + if (!receiveStream->Empty()) + serial.SendData(receiveStream->ExtractContiguousRange(std::numeric_limits::max()), [this]() + { + Subject().AckReceived(); + + infra::WeakPtr self = Subject().ObserverPtr(); + receiveStream = nullptr; + + if (self.lock()) + DataReceived(); + }); + else + receiveStream = nullptr; + } + } + + void TargetUartConnection::ReceivedSerialData() + { + if (!queue.Empty() && !streamRequested) + { + streamRequested = true; + Subject().RequestSendStream(Subject().MaxSendStreamSize()); + } + } +} diff --git a/postmaster/programmer/TargetUartConnection.hpp b/postmaster/programmer/TargetUartConnection.hpp new file mode 100644 index 0000000..919a242 --- /dev/null +++ b/postmaster/programmer/TargetUartConnection.hpp @@ -0,0 +1,34 @@ +#ifndef POSTMASTER_TARGET_UART_CONNECTION_HPP +#define POSTMASTER_TARGET_UART_CONNECTION_HPP + +#include "hal/interfaces/SerialCommunication.hpp" +#include "infra/event/QueueForOneReaderOneIrqWriter.hpp" +#include "services/network/Connection.hpp" + +namespace application +{ + class TargetUartConnection + : public services::ConnectionObserver + { + public: + template + using WithStorage = infra::WithStorage>; + + TargetUartConnection(infra::ByteRange buffer, hal::SerialCommunication& serial); + ~TargetUartConnection(); + + virtual void SendStreamAvailable(infra::SharedPtr&& streamWriter) override; + virtual void DataReceived() override; + + private: + void ReceivedSerialData(); + + private: + infra::QueueForOneReaderOneIrqWriter queue; + hal::SerialCommunication& serial; + infra::SharedPtr receiveStream; + bool streamRequested = false; + }; +} + +#endif diff --git a/postmaster/prototype_win/CMakeLists.txt b/postmaster/prototype_win/CMakeLists.txt new file mode 100644 index 0000000..2075e42 --- /dev/null +++ b/postmaster/prototype_win/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable(postmaster.prototype_win) +emil_build_for(postmaster.prototype_win HOST Windows) + +target_sources(postmaster.prototype_win PRIVATE + Main.cpp +) + +add_version_header_dependency(postmaster.prototype_win postmaster.version) + +target_link_libraries(postmaster.prototype_win PUBLIC + postmaster.instantiations + postmaster.programmer + hal.generic + hal.windows + services.network_instantiations +) diff --git a/postmaster/prototype_win/Main.cpp b/postmaster/prototype_win/Main.cpp new file mode 100644 index 0000000..c212059 --- /dev/null +++ b/postmaster/prototype_win/Main.cpp @@ -0,0 +1,156 @@ +#include "generated/Version.h" +#include "hal/generic/SynchronousRandomDataGeneratorGeneric.hpp" +#include "hal/generic/TimerServiceGeneric.hpp" +#include "hal/windows/UartWindows.hpp" +#include "postmaster/instantiations/EchoServer.hpp" +#include "postmaster/instantiations/HttpServer.hpp" +#include "postmaster/instantiations/Mdns.hpp" +#include "postmaster/instantiations/MdnsDiscovery.hpp" +#include "services/network_instantiations/NetworkAdapter.hpp" +#include "services/st_util/FlashOnStBootloaderCommunicator.hpp" +#include "services/st_util/StBootloaderCommunicatorUart.hpp" +#include "services/tracer/TracerOnIoOutputInfrastructure.hpp" + +namespace main_ +{ + struct Programmer + { + Programmer(application::HttpPageFirmware& page) + : page(page) + { + ReInitialize("Constructing"); + } + + ~Programmer() + { + page.ResetReceptor("Destructing"); + } + + void ReInitialize(infra::BoundedConstString reason) + { + page.ResetReceptor(reason); + state.Emplace([this]() + { + page.SetReceptor(state->firmwareReceptorToFlash); + }, + [this](infra::BoundedConstString reason) + { + ReInitialize(reason); + }); + } + + struct State + { + State(const infra::Function& onInitialized, const infra::Function& onError) + : communicator(serial, onInitialized, onError) + {} + + hal::UartWindows serial{ "COM11", []() + { + hal::UartWindows::Config config; + config.baudRate = CBR_57600; + config.parity = hal::UartWindows::Config::Parity::even; + return config; + }() }; + + services::StBootloaderCommunicatorUart communicator; + std::array sectors{ 0x8000, 0x8000, 0x8000, 0x8000, 0x20000, 0x40000, 0x40000, 0x40000, 0x40000, 0x40000, 0x40000, 0x40000 }; + services::FlashHeterogeneousOnStBootloaderCommunicator communicatorFlash{ sectors, communicator }; + application::FirmwareReceptorToFlash firmwareReceptorToFlash{ communicatorFlash }; + }; + + application::HttpPageFirmware& page; + infra::Optional state; + }; +} + +class ConfigurationStoreInterfaceStub + : public services::ConfigurationStoreInterface +{ +public: + virtual uint32_t Write() override + { + return 0; + } + + virtual void Unlocked() override + {} +}; + +class ResetStub + : public hal::Reset +{ +public: + void ResetModule(const char* resetReason) override + { + std::exit(EXIT_SUCCESS); + } +}; + +// clang-format off + +class FlashStub + : public hal::Flash +{ +public: + uint32_t NumberOfSectors() const { return 0; } + uint32_t SizeOfSector(uint32_t sectorIndex) const { return 0; } + uint32_t SectorOfAddress(uint32_t address) const { return 0; } + uint32_t AddressOfSector(uint32_t sectorIndex) const { return 0; } + void WriteBuffer(infra::ConstByteRange buffer, uint32_t address, infra::Function onDone) { onDone(); } + void ReadBuffer(infra::ByteRange buffer, uint32_t address, infra::Function onDone) { onDone(); } + void EraseSectors(uint32_t beginIndex, uint32_t endIndex, infra::Function onDone) { onDone(); } +}; + +class GpioPinStub + : public hal::GpioPin +{ +public: + bool Get() const { return false; } + void Set(bool value) {} + bool GetOutputLatch() const { return false; } + void SetAsInput() {} + bool IsInput() const { return false; } + void Config(hal::PinConfigType config) {} + void Config(hal::PinConfigType config, bool startOutputState) {} + void ResetConfig() {} + void EnableInterrupt(const infra::Function& action, hal::InterruptTrigger trigger, hal::InterruptType type = hal::InterruptType::dispatched) {} + void DisableInterrupt() {} +}; + +// clang-format on + +int main() +{ + static hal::TimerServiceGeneric timerService; + static hal::SynchronousRandomDataGeneratorGeneric randomDataGenerator; + static main_::NetworkAdapter networkAdapter; + static main_::TracerOnIoOutputInfrastructure tracer; + + static ConfigurationStoreInterfaceStub configurationStore; + static infra::BoundedString::WithStorage<32> hostname{ "postmaster-abc" }; + static infra::BoundedString::WithStorage<64> attributes{ "amp-example" }; + static infra::BoundedString::WithStorage<64> password{ "" }; + static services::ConfigurationStoreAccess hostnameAccess{ configurationStore, hostname }; + static services::ConfigurationStoreAccess attributesAccess{ configurationStore, attributes }; + static services::ConfigurationStoreAccess passwordAccess{ configurationStore, password }; + + infra::Creator serialCreator([](infra::Optional& value, const main_::UartConfig& config) + { + value.Emplace("COM13"); + }); + + static main_::Mdns mdns{ "richard", networkAdapter.DatagramFactory(), networkAdapter.Multicast(), services::IPv4Address{ 1, 2, 5, 6 }, Postmaster::generated::VERSION, Postmaster::generated::VERSION_FULL, "attributes" }; + static main_::MdnsDiscovery mdnsDiscovery{ networkAdapter.DatagramFactory(), networkAdapter.Multicast() }; + + static ResetStub reset; + static FlashStub flash; + static GpioPinStub resetTarget; + static GpioPinStub boot0; + static application::Authentication authentication{ passwordAccess, randomDataGenerator }; + static main_::HttpServer httpServer{ networkAdapter.ConnectionFactory(), hostnameAccess, attributesAccess, passwordAccess, authentication, mdnsDiscovery.discovery, serialCreator, serialCreator, flash, reset, [](bool open, services::IPAddress address) {}, [](bool receiving) {}, [](bool receiving) {}, resetTarget, boot0 }; + static main_::EchoServer echoServer{ networkAdapter.ConnectionFactory(), serialCreator, 1235 }; + static main_::SingleConnectionLink link(httpServer.server, echoServer.listener); + + networkAdapter.Run(); +} diff --git a/postmaster/upgrade_pack_builder/CMakeLists.txt b/postmaster/upgrade_pack_builder/CMakeLists.txt new file mode 100644 index 0000000..5058a1f --- /dev/null +++ b/postmaster/upgrade_pack_builder/CMakeLists.txt @@ -0,0 +1,10 @@ +add_executable(postmaster.upgrade_pack_builder) +emil_build_for(postmaster.upgrade_pack_builder HOST All) + +target_sources(postmaster.upgrade_pack_builder PRIVATE + Main.cpp +) + +target_link_libraries(postmaster.upgrade_pack_builder PUBLIC + upgrade.pack_builder_instantiations +) diff --git a/postmaster/upgrade_pack_builder/Main.cpp b/postmaster/upgrade_pack_builder/Main.cpp new file mode 100644 index 0000000..a0b4173 --- /dev/null +++ b/postmaster/upgrade_pack_builder/Main.cpp @@ -0,0 +1,13 @@ +#include "upgrade/pack_builder_instantiations/UpgradePackBuilderApplication.hpp" + +int main(int argc, const char* argv[]) +{ + application::UpgradePackBuilder::HeaderInfo header{ "Postmaster", "v0.1.0", "", 0 }; // x-release-please-version + application::SupportedTargets supportedTargets = application::SupportedTargets::Create() + .Mandatory() + .AddHex("app"); + + main_::UpgradePackBuilderApplication builder(header, supportedTargets); + + return builder.Main(argc, argv); +}